backtracie 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 14ea35393497d5cb266920ef4da5b21b116ef1c4c25624da3c52fcd89dddcd7d
4
- data.tar.gz: 4d954ba9c6527707fcec413b320f6a80fa11a42f19743a94ed79a6d42871b95e
3
+ metadata.gz: 887db5ff6ff55d3cf758d7ad192d5a01400a6c31ba6e46067073cf4bff0d34ac
4
+ data.tar.gz: 4b6b131ebfa4071b5dc222ca9758b30b22f8175c373de2813c85c2bd732a8a5c
5
5
  SHA512:
6
- metadata.gz: 072ac1862f8211ba37358f5d20dbc9ff3ffa176559d50d176d8d05f5e3940b9e18865e4a358b170915afa63980f3811deef6313400219048ce9587259e869a98
7
- data.tar.gz: 13db93efa6360ef1edb00277d7730fd837ba4aca67ca8a1bc8f61ecf8369d35968263630f5cbe458309452726d26ee03b57eab3e927987245b5b3b5f7c11f032
6
+ metadata.gz: 36e6e23f5eeeef37f14ca586891b9bdd06c44b5dc1b0ab984858f298ff87fb94f38eff9702521fb163587dd9771b73d3f208f98ce059d018a055830320fcc00d
7
+ data.tar.gz: 1b598ed425ae8ee01a51228c15886dc246c4324a8d4371c54fa36b49d6b66e08ffebff26af201cf3d756867aa7f3ae27dac791454984ebddb641ae0c693b021e
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,13 @@
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
+
7
11
  image:https://img.shields.io/badge/Contributor%20Covenant-2.0-4baaaa.svg["Contributor Covenant", link="CODE_OF_CONDUCT.adoc"]
8
12
  image:https://badge.fury.io/rb/backtracie.svg["Gem Version", link="https://badge.fury.io/rb/backtracie"]
13
+ 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
14
 
10
15
  [discrete]
11
16
  == Contents
@@ -39,7 +44,75 @@ This gem is versioned according to http://semver.org/spec/v2.0.0.html[Semantic V
39
44
 
40
45
  == Usage
41
46
 
42
- TODO
47
+ Currently, `backtracie` exposes two APIs:
48
+
49
+ * `Backtracie.backtrace_locations(thread)`: Returns an array representing the backtrace of the given `thread`. Similar to `Thread#backtrace_locations`.
50
+ * `Backtracie.caller_locations`: Returns an array representing the backtrace of the current thread, starting from the caller of the current method. the Similar to `Kernel#caller_locations`.
51
+
52
+ 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:
53
+
54
+ [source,ruby]
55
+ ----
56
+ $ bundle exec pry
57
+ [1] pry(main)> require 'backtracie'
58
+
59
+ [2] pry(main)> class Example
60
+ [2] pry(main)* def foo
61
+ [2] pry(main)* bar
62
+ [2] pry(main)* end
63
+ [2] pry(main)* def bar
64
+ [2] pry(main)* Backtracie.caller_locations.first
65
+ [2] pry(main)* end
66
+ [2] pry(main)* end
67
+
68
+ [3] pry(main)> Example.new.foo
69
+ => #<Backtracie::Location:0x0000561b236c9208
70
+ @absolute_path="(pry)",
71
+ @base_label="foo",
72
+ @debug=
73
+ {:ruby_frame?=>true,
74
+ :should_use_iseq=>false,
75
+ :should_use_cfunc_name=>false,
76
+ :vm_method_type=>0,
77
+ :line_number=>3,
78
+ :called_id=>:foo,
79
+ :defined_class_refinement?=>false,
80
+ :self_class=>Example,
81
+ :real_class=>Example,
82
+ :self=>#<Example:0x0000561b236f3a08>,
83
+ :self_class_singleton?=>false,
84
+ :iseq_is_block?=>false,
85
+ :iseq_is_eval?=>false,
86
+ :cfunc_name=>nil,
87
+ :iseq=>
88
+ {:path=>"(pry)",
89
+ :absolute_path=>"(pry)",
90
+ :label=>"foo",
91
+ :base_label=>"foo",
92
+ :full_label=>"foo",
93
+ :first_lineno=>2,
94
+ :classpath=>nil,
95
+ :singleton_method_p=>false,
96
+ :method_name=>"foo",
97
+ :qualified_method_name=>"foo"},
98
+ :callable_method_entry=>
99
+ {:path=>"(pry)",
100
+ :absolute_path=>"(pry)",
101
+ :label=>"foo",
102
+ :base_label=>"foo",
103
+ :full_label=>"Example#foo",
104
+ :first_lineno=>2,
105
+ :classpath=>"Example",
106
+ :singleton_method_p=>false,
107
+ :method_name=>"foo",
108
+ :qualified_method_name=>"Example#foo"}},
109
+ @label="foo",
110
+ @lineno=3,
111
+ @path="(pry)",
112
+ @qualified_method_name="Example#foo">
113
+ ----
114
+
115
+ 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
116
 
44
117
  == Development
45
118
 
@@ -49,13 +122,16 @@ To open a console with the gem loaded, run `bundle console`.
49
122
 
50
123
  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
124
 
125
+ 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`.
126
+ To test on all rubies using docker, you can use `bundle exec rake test-all`.
127
+
52
128
  == Feedback and success stories
53
129
 
54
130
  Your feedback is welcome!
55
131
 
56
132
  == Contributing
57
133
 
58
- Bug reports and pull requests are welcome on GitLab at https://github.com/ivoanjo/backtracie.
134
+ Bug reports and pull requests are welcome at https://github.com/ivoanjo/backtracie.
59
135
 
60
136
  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
137
 
@@ -64,3 +140,27 @@ Maintained with ❤️ by https://ivoanjo.me/[Ivo Anjo].
64
140
  == Code of Conduct
65
141
 
66
142
  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].
143
+
144
+ == Interesting Links
145
+
146
+ Here's some gems that are doing similar things to `backtracie`:
147
+
148
+ * https://github.com/tmm1/stackprof: A sampling call-stack profiler for Ruby
149
+ * https://github.com/ko1/pretty_backtrace: Pretty your exception backtrace
150
+ * 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
151
+
152
+ Other interesting links on this matter:
153
+
154
+ * https://github.com/ruby/ruby/pull/3299: vm_backtrace.c: let rb_profile_frames show cfunc frames
155
+ * https://github.com/ruby/ruby/pull/2713: Fix use of the rb_profile_frames start parameter
156
+ * 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.
157
+ * https://github.com/ko1/rubyhackchallenge: "Ruby Hack Challenge" (RHC) is a short guide to hack MRI (Matz Ruby Interpreter) internals
158
+ * https://docs.ruby-lang.org/en/3.0.0/doc/extension_rdoc.html: Creating Extension Libraries for Ruby
159
+ * https://ruby-hacking-guide.github.io/: Ruby Hacking Guide
160
+ ** This is one of the most through and deep guides out there to the MRI internals. Very detailed and in depth, but outdated.
161
+ * http://patshaughnessy.net/ruby-under-a-microscope: Book with a really good introduction to MRI internals.
162
+
163
+ My blog posts on better backtraces:
164
+
165
+ * https://ivoanjo.me/blog/2020/07/05/ruby-experiment-include-class-names-in-backtraces/: ruby experiment: include class names in backtraces
166
+ * 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,18 @@ 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"]
43
49
  end
@@ -16,44 +16,56 @@
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;
36
+ static ID current_id;
37
+ static ID backtrace_locations_id;
32
38
  static VALUE backtracie_module = Qnil;
33
39
  static VALUE backtracie_location_class = Qnil;
34
40
 
35
- // Function headers
36
-
37
41
  static VALUE primitive_caller_locations(VALUE self);
38
42
  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))
43
+ static VALUE collect_backtrace_locations(VALUE self, VALUE thread, int ignored_stack_top_frames);
44
+ inline static VALUE new_location(VALUE absolute_path, VALUE base_label, VALUE label, VALUE lineno, VALUE path, VALUE qualified_method_name, VALUE debug);
45
+ static VALUE ruby_frame_to_location(raw_location *the_location);
46
+ static VALUE qualified_method_name_for_location(raw_location *the_location);
47
+ static VALUE cfunc_frame_to_location(raw_location *the_location, raw_location *last_ruby_location);
48
+ static VALUE frame_from_location(raw_location *the_location);
49
+ static VALUE qualified_method_name_for_block(raw_location *the_location);
50
+ static VALUE qualified_method_name_from_self(raw_location *the_location);
51
+ static bool is_self_class_singleton(raw_location *the_location);
52
+ static bool is_defined_class_a_refinement(raw_location *the_location);
53
+ static VALUE debug_raw_location(raw_location *the_location);
54
+ static VALUE debug_frame(VALUE frame);
55
+ static inline VALUE to_boolean(bool value) ;
49
56
 
50
57
  void Init_backtracie_native_extension(void) {
58
+ main_object_instance = backtracie_rb_vm_top_self();
59
+ ensure_object_is_thread_id = rb_intern("ensure_object_is_thread");
60
+ to_s_id = rb_intern("to_s");
61
+ current_id = rb_intern("current");
62
+ backtrace_locations_id = rb_intern("backtrace_locations");
63
+
51
64
  backtracie_module = rb_const_get(rb_cObject, rb_intern("Backtracie"));
52
65
  rb_global_variable(&backtracie_module);
53
66
 
54
67
  rb_define_module_function(backtracie_module, "backtrace_locations", primitive_backtrace_locations, 1);
55
68
 
56
- // We need to keep a reference to Backtracie::Locations around, to create new instances
57
69
  backtracie_location_class = rb_const_get(backtracie_module, rb_intern("Location"));
58
70
  rb_global_variable(&backtracie_location_class);
59
71
 
@@ -63,24 +75,30 @@ void Init_backtracie_native_extension(void) {
63
75
  }
64
76
 
65
77
  // 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) {
78
+ static VALUE collect_backtrace_locations(VALUE self, VALUE thread, int ignored_stack_top_frames) {
67
79
  int stack_depth = 0;
68
- VALUE frames[MAX_STACK_DEPTH];
69
- VALUE correct_labels[MAX_STACK_DEPTH];
70
- int lines[MAX_STACK_DEPTH];
80
+ raw_location raw_locations[MAX_STACK_DEPTH];
71
81
 
72
- if (thread == Qnil) {
73
- // Get for own thread
74
- stack_depth = modified_rb_profile_frames(0, MAX_STACK_DEPTH, frames, correct_labels, lines);
75
- } else {
76
- stack_depth = modified_rb_profile_frames_for_thread(thread, 0, MAX_STACK_DEPTH, frames, correct_labels, lines);
77
- }
78
-
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
+ #ifndef PRE_MJIT_RUBY
83
+ if (thread == Qnil) {
84
+ // Get for own thread
85
+ stack_depth = backtracie_rb_profile_frames(MAX_STACK_DEPTH, raw_locations);
86
+ } else {
87
+ stack_depth = backtracie_rb_profile_frames_for_thread(thread, MAX_STACK_DEPTH, raw_locations);
88
+ }
89
+ #else
90
+ VALUE current_thread = rb_funcall(rb_cThread, current_id, 0);
91
+
92
+ if (thread == Qnil || thread == current_thread) {
93
+ thread = current_thread;
94
+ // Since we're going to sample the current thread, we don't want Thread#backtrace_locations to show up
95
+ // on the stack (as we want to match the exact depth we get from MRI), so we ignore one extra frame
96
+ ignored_stack_top_frames++;
97
+ }
82
98
 
83
- stack_depth -= ignored_stack_bottom_frames;
99
+ VALUE locations_array = rb_funcall(thread, backtrace_locations_id, 0);
100
+ stack_depth = backtracie_profile_frames_from_ruby_locations(locations_array, raw_locations);
101
+ #endif
84
102
 
85
103
  VALUE locations = rb_ary_new_capa(stack_depth - ignored_stack_top_frames);
86
104
 
@@ -88,22 +106,16 @@ static VALUE caller_locations(VALUE self, VALUE thread, int ignored_stack_top_fr
88
106
  // Kernel#caller_locations is to instead use the path and line number of the last Ruby frame seen.
89
107
  // Thus, we keep that frame here to able to replicate that behavior.
90
108
  // (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;
109
+ raw_location *last_ruby_location = 0;
93
110
 
94
111
  for (int i = stack_depth - 1; i >= ignored_stack_top_frames; i--) {
95
- VALUE frame = frames[i];
96
- int line = lines[i];
97
-
98
112
  VALUE location = Qnil;
99
113
 
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]);
114
+ if (raw_locations[i].is_ruby_frame) {
115
+ last_ruby_location = &raw_locations[i];
116
+ location = ruby_frame_to_location(&raw_locations[i]);
105
117
  } else {
106
- location = cfunc_frame_to_location(frame, last_ruby_frame, last_ruby_line);
118
+ location = cfunc_frame_to_location(&raw_locations[i], last_ruby_location);
107
119
  }
108
120
 
109
121
  rb_ary_store(locations, i - ignored_stack_top_frames, location);
@@ -119,67 +131,241 @@ static VALUE primitive_caller_locations(VALUE self) {
119
131
  // * the frame from the caller itself (since we're replicating the semantics of Kernel#caller_locations)
120
132
  int ignored_stack_top_frames = 3;
121
133
 
122
- return caller_locations(self, Qnil, ignored_stack_top_frames);
134
+ return collect_backtrace_locations(self, Qnil, ignored_stack_top_frames);
123
135
  }
124
136
 
125
137
  static VALUE primitive_backtrace_locations(VALUE self, VALUE thread) {
126
- rb_funcall(backtracie_module, rb_intern("ensure_object_is_thread"), 1, thread);
138
+ rb_funcall(backtracie_module, ensure_object_is_thread_id, 1, thread);
127
139
 
128
140
  int ignored_stack_top_frames = 0;
129
141
 
130
- return caller_locations(self, thread, ignored_stack_top_frames);
142
+ return collect_backtrace_locations(self, thread, ignored_stack_top_frames);
131
143
  }
132
144
 
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 };
145
+ inline static VALUE new_location(
146
+ VALUE absolute_path,
147
+ VALUE base_label,
148
+ VALUE label,
149
+ VALUE lineno,
150
+ VALUE path,
151
+ VALUE qualified_method_name,
152
+ VALUE debug
153
+ ) {
154
+ VALUE arguments[] = { absolute_path, base_label, label, lineno, path, qualified_method_name, debug };
135
155
  return rb_class_new_instance(VALUE_COUNT(arguments), arguments, backtracie_location_class);
136
156
  }
137
157
 
138
- static bool is_ruby_frame(VALUE frame) {
139
- VALUE absolute_path = rb_profile_frame_absolute_path(frame);
158
+ static VALUE ruby_frame_to_location(raw_location *the_location) {
159
+ VALUE frame = frame_from_location(the_location);
140
160
 
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
161
  return new_location(
147
162
  rb_profile_frame_absolute_path(frame),
148
163
  rb_profile_frame_base_label(frame),
149
- rb_profile_frame_label(correct_label),
150
- last_ruby_line,
164
+ rb_profile_frame_label(the_location->iseq),
165
+ INT2FIX(the_location->line_number),
151
166
  rb_profile_frame_path(frame),
152
- debug_frame(frame, rb_str_new2("ruby_frame"))
167
+ qualified_method_name_for_location(the_location),
168
+ debug_raw_location(the_location)
153
169
  );
154
170
  }
155
171
 
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
172
+ static VALUE qualified_method_name_for_location(raw_location *the_location) {
173
+ VALUE frame = frame_from_location(the_location);
174
+ VALUE defined_class = backtracie_defined_class(the_location);
175
+ VALUE qualified_method_name = Qnil;
176
+
177
+ if (is_defined_class_a_refinement(the_location)) {
178
+ qualified_method_name = backtracie_refinement_name(the_location);
179
+ rb_str_concat(qualified_method_name, rb_str_new2("#"));
180
+ rb_str_concat(qualified_method_name, rb_profile_frame_label(frame));
181
+ } else if (is_self_class_singleton(the_location)) {
182
+ qualified_method_name = qualified_method_name_from_self(the_location);
183
+ } else if (backtracie_iseq_is_block(the_location) || backtracie_iseq_is_eval(the_location)) {
184
+ qualified_method_name = qualified_method_name_for_block(the_location);
185
+ } else if (defined_class != Qnil && rb_mod_name(defined_class) == Qnil) {
186
+ // Instance of an anonymous class. Let's find it a name
187
+ VALUE superclass = defined_class;
188
+ VALUE superclass_name = Qnil;
189
+ do {
190
+ superclass = RCLASS_SUPER(superclass);
191
+ superclass_name = rb_mod_name(superclass);
192
+ } while (superclass_name == Qnil);
193
+
194
+ qualified_method_name = rb_str_new2("");
195
+ rb_str_concat(qualified_method_name, superclass_name);
196
+ rb_str_concat(qualified_method_name, rb_str_new2("$anonymous#"));
197
+ rb_str_concat(qualified_method_name, rb_profile_frame_base_label(frame_from_location(the_location)));
198
+ } else {
199
+ qualified_method_name = backtracie_rb_profile_frame_qualified_method_name(frame);
200
+
201
+ if (qualified_method_name == Qnil) {
202
+ qualified_method_name = qualified_method_name_from_self(the_location);
203
+ }
204
+ }
205
+
206
+ if (backtracie_iseq_is_block(the_location) || backtracie_iseq_is_eval(the_location)) {
207
+ rb_str_concat(qualified_method_name, rb_str_new2("{block}"));
208
+ }
209
+
210
+ return qualified_method_name;
211
+ }
212
+
213
+ static VALUE cfunc_frame_to_location(raw_location *the_location, raw_location *last_ruby_location) {
214
+ VALUE last_ruby_frame =
215
+ last_ruby_location != 0 ? frame_from_location(last_ruby_location) : Qnil;
216
+
217
+ // Replaces label and base_label in cfuncs
218
+ VALUE method_name =
219
+ the_location->should_use_cfunc_name ?
220
+ the_location->cfunc_name : backtracie_rb_profile_frame_method_name(the_location->callable_method_entry);
158
221
 
159
222
  return new_location(
160
- last_ruby_frame != Qnil ? rb_profile_frame_absolute_path(last_ruby_frame) : Qnil,
223
+ SAFE_NAVIGATION(rb_profile_frame_absolute_path, last_ruby_frame),
161
224
  method_name,
162
225
  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"))
226
+ last_ruby_location != 0 ? INT2FIX(last_ruby_location->line_number) : Qnil,
227
+ SAFE_NAVIGATION(rb_profile_frame_path, last_ruby_frame),
228
+ backtracie_rb_profile_frame_qualified_method_name(the_location->callable_method_entry),
229
+ debug_raw_location(the_location)
166
230
  );
167
231
  }
168
232
 
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) {
233
+ static VALUE frame_from_location(raw_location *the_location) {
234
+ return \
235
+ the_location->should_use_iseq ||
236
+ // This one is somewhat weird, but the regular MRI Ruby APIs seem to pick the iseq for evals as well
237
+ backtracie_iseq_is_eval(the_location) ?
238
+ the_location->iseq : the_location->callable_method_entry;
239
+ }
240
+
241
+ static VALUE qualified_method_name_for_block(raw_location *the_location) {
242
+ VALUE class_name = backtracie_rb_profile_frame_classpath(the_location->callable_method_entry);
243
+ VALUE method_name = backtracie_called_id(the_location);
244
+ VALUE is_singleton_method = rb_profile_frame_singleton_method_p(the_location->iseq);
245
+
246
+ VALUE name = rb_str_new2("");
247
+ rb_str_concat(name, class_name);
248
+ rb_str_concat(name, is_singleton_method ? rb_str_new2(".") : rb_str_new2("#"));
249
+ rb_str_concat(name, rb_sym2str(method_name));
250
+
251
+ return name;
252
+ }
253
+
254
+ static VALUE qualified_method_name_from_self(raw_location *the_location) {
255
+ if (the_location->self == Qnil) return Qnil;
256
+
257
+ VALUE self_class = rb_class_of(the_location->self);
258
+ bool is_self_class_singleton = FL_TEST(self_class, FL_SINGLETON);
259
+
260
+ VALUE name = rb_str_new2("");
261
+ if (is_self_class_singleton) {
262
+ if (the_location->self == main_object_instance) {
263
+ rb_str_concat(name, rb_str_new2("Object$<main>#"));
264
+ } else {
265
+ // Crawl up the hierarchy to find a real class
266
+ VALUE the_class = rb_class_real(self_class);
267
+
268
+ // This is similar to BetterBacktraceHelper's self_class_module_or_class?
269
+ // If the real class of this object is the actual class Class or the class Module,
270
+ // it means that this is a method being directly called on a given Class/Module,
271
+ // e.g. Kernel.puts or BasicObject.name. In this case, Ruby already sets the name
272
+ // correctly, so we just delegate.
273
+ if (the_class == rb_cModule || the_class == rb_cClass) {
274
+ // Is the class/module is anonymous?
275
+ if (rb_mod_name(the_location->self) == Qnil) {
276
+ rb_str_concat(name, rb_funcall(the_class, to_s_id, 0));
277
+ rb_str_concat(name, rb_str_new2("$singleton."));
278
+ } else {
279
+ return SAFE_NAVIGATION(backtracie_rb_profile_frame_qualified_method_name, the_location->callable_method_entry);
280
+ }
281
+ } else {
282
+ rb_str_concat(name, rb_funcall(the_class, to_s_id, 0));
283
+ rb_str_concat(name, rb_str_new2("$singleton#"));
284
+ }
285
+ }
286
+ } else {
287
+ // Not very sure if this branch of the if is ever reached, and if it would be for a instance or static call, so
288
+ // let's just have these defaults and revisit as needed
289
+ rb_str_concat(name, rb_funcall(self_class, to_s_id, 0));
290
+ rb_str_concat(name, rb_str_new2("#"));
291
+ }
292
+
293
+ if (backtracie_iseq_is_block(the_location) || backtracie_iseq_is_eval(the_location)) {
294
+ // Nothing to do, {block} will be appended in qualified_method_name_for_location which called us
295
+ } else {
296
+ rb_str_concat(name, rb_profile_frame_base_label(frame_from_location(the_location)));
297
+ }
298
+
299
+ return name;
300
+ }
301
+
302
+ static bool is_self_class_singleton(raw_location *the_location) {
303
+ return the_location->self != Qnil && FL_TEST(rb_class_of(the_location->self), FL_SINGLETON);
304
+ }
305
+
306
+ static bool is_defined_class_a_refinement(raw_location *the_location) {
307
+ VALUE defined_class = backtracie_defined_class(the_location);
308
+
309
+ return defined_class != Qnil && FL_TEST(rb_class_of(defined_class), RMODULE_IS_REFINEMENT);
310
+ }
311
+
312
+ static VALUE debug_raw_location(raw_location *the_location) {
313
+ VALUE self_class = SAFE_NAVIGATION(rb_class_of, the_location->self);
314
+
171
315
  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
316
+ ID2SYM(rb_intern("ruby_frame?")) , /* => */ to_boolean(the_location->is_ruby_frame),
317
+ ID2SYM(rb_intern("should_use_iseq")), /* => */ to_boolean(the_location->should_use_iseq),
318
+ ID2SYM(rb_intern("should_use_cfunc_name")), /* => */ to_boolean(the_location->should_use_cfunc_name),
319
+ ID2SYM(rb_intern("vm_method_type")), /* => */ INT2FIX(the_location->vm_method_type),
320
+ ID2SYM(rb_intern("line_number")), /* => */ INT2FIX(the_location->line_number),
321
+ ID2SYM(rb_intern("called_id")), /* => */ backtracie_called_id(the_location),
322
+ // TODO: On Ruby < 3.0, running be pry -e 'require "backtracie"; Backtracie.caller_locations' with this being
323
+ // exposed causes a VM segfault inside the pretty printing code
324
+ // ID2SYM(rb_intern("defined_class")), /* => */ backtracie_defined_class(the_location),
325
+ ID2SYM(rb_intern("defined_class_refinement?")), /* => */ to_boolean(is_defined_class_a_refinement(the_location)),
326
+ ID2SYM(rb_intern("self_class")), /* => */ self_class,
327
+ ID2SYM(rb_intern("real_class")), /* => */ SAFE_NAVIGATION(rb_class_real, self_class),
328
+ ID2SYM(rb_intern("self")), /* => */ the_location->self,
329
+ ID2SYM(rb_intern("self_class_singleton?")), /* => */ to_boolean(is_self_class_singleton(the_location)),
330
+ ID2SYM(rb_intern("iseq_is_block?")), /* => */ to_boolean(backtracie_iseq_is_block(the_location)),
331
+ ID2SYM(rb_intern("iseq_is_eval?")), /* => */ to_boolean(backtracie_iseq_is_eval(the_location)),
332
+ ID2SYM(rb_intern("cfunc_name")), /* => */ the_location->cfunc_name,
333
+ ID2SYM(rb_intern("iseq")), /* => */ debug_frame(the_location->iseq),
334
+ ID2SYM(rb_intern("callable_method_entry")), /* => */ debug_frame(the_location->callable_method_entry)
335
+ };
336
+
337
+ VALUE debug_hash = rb_hash_new();
338
+ #ifndef PRE_MJIT_RUBY
339
+ // FIXME: Hack, need to actually fix this
340
+ rb_hash_bulk_insert(VALUE_COUNT(arguments), arguments, debug_hash);
341
+ #endif
342
+ return debug_hash;
343
+ }
344
+
345
+ static VALUE debug_frame(VALUE frame) {
346
+ if (frame == Qnil) return Qnil;
347
+
348
+ VALUE arguments[] = {
349
+ ID2SYM(rb_intern("path")), /* => */ rb_profile_frame_path(frame),
350
+ ID2SYM(rb_intern("absolute_path")), /* => */ rb_profile_frame_absolute_path(frame),
351
+ ID2SYM(rb_intern("label")), /* => */ rb_profile_frame_label(frame),
352
+ ID2SYM(rb_intern("base_label")), /* => */ rb_profile_frame_base_label(frame),
353
+ ID2SYM(rb_intern("full_label")), /* => */ rb_profile_frame_full_label(frame),
354
+ ID2SYM(rb_intern("first_lineno")), /* => */ rb_profile_frame_first_lineno(frame),
355
+ ID2SYM(rb_intern("classpath")), /* => */ backtracie_rb_profile_frame_classpath(frame),
356
+ ID2SYM(rb_intern("singleton_method_p")), /* => */ rb_profile_frame_singleton_method_p(frame),
357
+ ID2SYM(rb_intern("method_name")), /* => */ rb_profile_frame_method_name(frame),
358
+ ID2SYM(rb_intern("qualified_method_name")), /* => */ backtracie_rb_profile_frame_qualified_method_name(frame)
183
359
  };
184
- return rb_ary_new_from_values(VALUE_COUNT(arguments), arguments);
360
+
361
+ VALUE debug_hash = rb_hash_new();
362
+ #ifndef PRE_MJIT_RUBY
363
+ // FIXME: Hack, need to actually fix this
364
+ rb_hash_bulk_insert(VALUE_COUNT(arguments), arguments, debug_hash);
365
+ #endif
366
+ return debug_hash;
367
+ }
368
+
369
+ static inline VALUE to_boolean(bool value) {
370
+ return value ? Qtrue : Qfalse;
185
371
  }