backtracie 0.1.0 → 0.2.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.
@@ -18,11 +18,55 @@
18
18
  # You should have received a copy of the GNU Lesser General Public License
19
19
  # along with backtracie. If not, see <http://www.gnu.org/licenses/>.
20
20
 
21
+ if ["jruby", "truffleruby"].include?(RUBY_ENGINE)
22
+ raise \
23
+ "\n#{"-" * 80}\nSorry! This gem is unsupported on #{RUBY_ENGINE}. Since it relies on a lot of guts of MRI Ruby, " \
24
+ "it's impossible to make a direct port.\n" \
25
+ "Perhaps a #{RUBY_ENGINE} equivalent could be created -- help is welcome! :)\n#{"-" * 80}"
26
+ end
27
+
21
28
  require "mkmf"
22
29
 
23
30
  # This warning gets really annoying when we include the Ruby mjit header file,
24
31
  # let's omit it
25
32
  $CFLAGS << " " << "-Wno-unused-function"
26
33
 
34
+ # Really dislike the "define everything at the beginning of the function" thing, sorry!
35
+ $CFLAGS << " " << "-Wno-declaration-after-statement"
36
+
37
+ # Enable us to use """modern""" C
38
+ if RUBY_VERSION < "2.4"
39
+ $CFLAGS << " " << "-std=c99"
40
+ end
41
+
42
+ # On older Rubies, we need to enable a few backports. See cfunc_frames_backport.h for details.
43
+ if RUBY_VERSION < "3"
44
+ $defs << "-DCFUNC_FRAMES_BACKPORT_NEEDED"
45
+ end
46
+
47
+ # Backport https://github.com/ruby/ruby/pull/3084 (present in 2.7 and 3.0) to Ruby 2.6
48
+ if RUBY_VERSION.start_with?("2.6")
49
+ $defs << "-DCLASSPATH_BACKPORT_NEEDED"
50
+ end
51
+
52
+ if RUBY_VERSION < "2.6"
53
+ $defs << "-DPRE_MJIT_RUBY"
54
+ end
55
+
27
56
  create_header
57
+
58
+ # The Ruby MJIT header is always (afaik?) suffixed with the exact RUBY version,
59
+ # including patch (e.g. 2.7.2). Thus, we add to the header file a definition
60
+ # containing the exact file, so that it can be used in a #include in the C code.
61
+ header_contents =
62
+ File.read($extconf_h)
63
+ .sub("#endif",
64
+ <<~EXTCONF_H.strip
65
+ #define RUBY_MJIT_HEADER "rb_mjit_min_header-#{RUBY_VERSION}.h"
66
+
67
+ #endif
68
+ EXTCONF_H
69
+ )
70
+ File.open($extconf_h, "w") { |file| file.puts(header_contents) }
71
+
28
72
  create_makefile "backtracie_native_extension"
@@ -0,0 +1,585 @@
1
+ // backtracie: Ruby gem for beautiful backtraces
2
+ // Copyright (C) 2021 Ivo Anjo <ivo@ivoanjo.me>
3
+ //
4
+ // This file is part of backtracie.
5
+ //
6
+ // backtracie is free software: you can redistribute it and/or modify
7
+ // it under the terms of the GNU Lesser General Public License as published by
8
+ // the Free Software Foundation, either version 3 of the License, or
9
+ // (at your option) any later version.
10
+ //
11
+ // backtracie is distributed in the hope that it will be useful,
12
+ // but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ // GNU Lesser General Public License for more details.
15
+ //
16
+ // You should have received a copy of the GNU Lesser General Public License
17
+ // along with backtracie. If not, see <http://www.gnu.org/licenses/>.
18
+
19
+ // -----------------------------------------------------------------------------
20
+ // The file below has modified versions of code extracted from the Ruby project.
21
+ // The Ruby project copyright and license follow:
22
+ // -----------------------------------------------------------------------------
23
+
24
+ // Ruby is copyrighted free software by Yukihiro Matsumoto <matz@netlab.jp>.
25
+ // You can redistribute it and/or modify it under either the terms of the
26
+ // 2-clause BSDL (see the file BSDL), or the conditions below:
27
+
28
+ // 1. You may make and give away verbatim copies of the source form of the
29
+ // software without restriction, provided that you duplicate all of the
30
+ // original copyright notices and associated disclaimers.
31
+
32
+ // 2. You may modify your copy of the software in any way, provided that
33
+ // you do at least ONE of the following:
34
+
35
+ // a. place your modifications in the Public Domain or otherwise
36
+ // make them Freely Available, such as by posting said
37
+ // modifications to Usenet or an equivalent medium, or by allowing
38
+ // the author to include your modifications in the software.
39
+
40
+ // b. use the modified software only within your corporation or
41
+ // organization.
42
+
43
+ // c. give non-standard binaries non-standard names, with
44
+ // instructions on where to get the original software distribution.
45
+
46
+ // d. make other distribution arrangements with the author.
47
+
48
+ // 3. You may distribute the software in object code or binary form,
49
+ // provided that you do at least ONE of the following:
50
+
51
+ // a. distribute the binaries and library files of the software,
52
+ // together with instructions (in the manual page or equivalent)
53
+ // on where to get the original distribution.
54
+
55
+ // b. accompany the distribution with the machine-readable source of
56
+ // the software.
57
+
58
+ // c. give non-standard binaries non-standard names, with
59
+ // instructions on where to get the original software distribution.
60
+
61
+ // d. make other distribution arrangements with the author.
62
+
63
+ // 4. You may modify and include the part of the software into any other
64
+ // software (possibly commercial). But some files in the distribution
65
+ // are not written by the author, so that they are not under these terms.
66
+
67
+ // For the list of those files and their copying conditions, see the
68
+ // file LEGAL.
69
+
70
+ // 5. The scripts and library files supplied as input to or produced as
71
+ // output from the software do not automatically fall under the
72
+ // copyright of the software, but belong to whomever generated them,
73
+ // and may be sold commercially, and may be aggregated with this
74
+ // software.
75
+
76
+ // 6. THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR
77
+ // IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
78
+ // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
79
+ // PURPOSE.
80
+
81
+ // -----------------------------------------------------------------------------
82
+
83
+ // ruby_shards.c contains a number of borrowed functions from the MRI Ruby (usually 3.0.0) source tree:
84
+ // * A few were copy-pasted verbatim, and are dependencies for other functions
85
+ // * A few were copy-pasted and then changed so we can add features and fixes
86
+ // * A few were copy-pasted verbatim, with the objective of backporting their 3.0.0 behavior to earlier Ruby versions
87
+ // `git blame` usually documents which functions were added for what reason.
88
+
89
+ // Note that since the RUBY_MJIT_HEADER is a very special header, meant for internal use only, it has a number of quirks:
90
+ //
91
+ // 1. I've seen a few segfaults when trying to call back into original Ruby functions. E.g. even if the API is used
92
+ // correctly, just the mere inclusion of RUBY_MJIT_HEADER causes usage to crash. Thus, as much as possible, it's
93
+ // better to define functions OUTSIDE this file.
94
+ //
95
+ // 2. On Windows, I've observed "multiple definition of `something...'" (such as `rb_vm_ep_local_ep') whenever there
96
+ // are multiple files in the codebase that include the RUBY_MJIT_HEADER.
97
+ // It looks like (some?) Windows versions of Ruby define a bunch of functions in the RUBY_MJIT_HEADER itself
98
+ // without marking them as "static" (e.g. not visible to the outside of the file), and thus the linker then complains
99
+ // when linking together several files which all have these non-private symbols.
100
+ // One possible hacky solution suggested by the internets is to use the "-Wl,-allow-multiple-definition" linker
101
+ // flags to ignore this problem; instead I've chosen to implement all usage of the RUBY_MJIT_HEADER on this file --
102
+ // no other file in backtracie shall include RUBY_MJIT_HEADER.
103
+ // It's a simpler approach, and hopefully avoids any problems.
104
+
105
+ #include "extconf.h"
106
+
107
+ #ifndef PRE_MJIT_RUBY
108
+
109
+ #ifndef RUBY_MJIT_HEADER_INCLUDED
110
+ #define RUBY_MJIT_HEADER_INCLUDED
111
+ #include RUBY_MJIT_HEADER
112
+ #endif
113
+
114
+ #include "ruby_shards.h"
115
+
116
+ /**********************************************************************
117
+ vm_backtrace.c -
118
+ $Author: ko1 $
119
+ created at: Sun Jun 03 00:14:20 2012
120
+ Copyright (C) 1993-2012 Yukihiro Matsumoto
121
+ **********************************************************************/
122
+
123
+ inline static int
124
+ calc_lineno(const rb_iseq_t *iseq, const VALUE *pc)
125
+ {
126
+ VM_ASSERT(iseq);
127
+ VM_ASSERT(iseq->body);
128
+ VM_ASSERT(iseq->body->iseq_encoded);
129
+ VM_ASSERT(iseq->body->iseq_size);
130
+ if (! pc) {
131
+ /* This can happen during VM bootup. */
132
+ VM_ASSERT(iseq->body->type == ISEQ_TYPE_TOP);
133
+ VM_ASSERT(! iseq->body->local_table);
134
+ VM_ASSERT(! iseq->body->local_table_size);
135
+ return 0;
136
+ }
137
+ else {
138
+ ptrdiff_t n = pc - iseq->body->iseq_encoded;
139
+ VM_ASSERT(n <= iseq->body->iseq_size);
140
+ VM_ASSERT(n >= 0);
141
+ ASSUME(n >= 0);
142
+ size_t pos = n; /* no overflow */
143
+ if (LIKELY(pos)) {
144
+ /* use pos-1 because PC points next instruction at the beginning of instruction */
145
+ pos--;
146
+ }
147
+ return rb_iseq_line_no(iseq, pos);
148
+ }
149
+ }
150
+
151
+ static VALUE
152
+ id2str(ID id)
153
+ {
154
+ VALUE str = rb_id2str(id);
155
+ if (!str) return Qnil;
156
+ return str;
157
+ }
158
+ #define rb_id2str(id) id2str(id)
159
+
160
+ // Hacked version of Ruby's rb_profile_frames from Ruby 3.0.0 with the following changes:
161
+ // 1. Instead of just using the rb_execution_context_t for the current thread, the context is received as an argument,
162
+ // thus allowing the sampling of any thread in the VM, not just the current one.
163
+ // 2. It gathers a lot more data: originally you'd get only a VALUE (either iseq or the cme, depending on the case),
164
+ // and the line number. The hacked version returns a whole raw_location with a lot more info.
165
+ // 3. It correctly ignores the dummy frame at the bottom of the main Ruby thread stack, thus mimicking the behavior of
166
+ // Ruby's backtrace_each (which is the function that is used to implement Thread#backtrace and friends)
167
+ // 4. Removed the start argument (upstream was broken anyway -- https://github.com/ruby/ruby/pull/2713 -- so we can
168
+ // re-add later if needed)
169
+ static int backtracie_rb_profile_frames_for_execution_context(
170
+ rb_execution_context_t *ec,
171
+ int limit,
172
+ raw_location *raw_locations
173
+ ) {
174
+ int i = 0;
175
+ const rb_control_frame_t *cfp = ec->cfp;
176
+ const rb_control_frame_t *end_cfp = RUBY_VM_END_CONTROL_FRAME(ec);
177
+ const rb_callable_method_entry_t *cme = 0;
178
+
179
+ // Hack #3 above: Here we go back one frame in addition to what the original Ruby rb_profile_frames method did.
180
+ // Why? According to backtrace_each() in vm_backtrace.c there's two "dummy frames" (what MRI calls them) at the
181
+ // bottom of the stack, and we need to skip them both.
182
+ // I have no idea why the original rb_profile_frames omits this. Without this, sampling `Thread.main` always
183
+ // returned one more frame than the regular MRI APIs (which use the aforementioned backtrace_each internally).
184
+ end_cfp = RUBY_VM_NEXT_CONTROL_FRAME(end_cfp);
185
+
186
+ for (i = 0; i < limit && cfp != end_cfp;) {
187
+ // Initialize the raw_location, to avoid issues
188
+ raw_locations[i].is_ruby_frame = false;
189
+ raw_locations[i].should_use_iseq = false;
190
+ raw_locations[i].should_use_cfunc_name = false;
191
+ raw_locations[i].vm_method_type = 0;
192
+ raw_locations[i].line_number = 0;
193
+ raw_locations[i].iseq = Qnil;
194
+ raw_locations[i].callable_method_entry = Qnil;
195
+ raw_locations[i].cfunc_name = Qnil;
196
+
197
+ // The current object this is getting called on!
198
+ raw_locations[i].self = cfp->self;
199
+
200
+ cme = rb_vm_frame_method_entry(cfp);
201
+
202
+ if (VM_FRAME_RUBYFRAME_P(cfp)) {
203
+ raw_locations[i].is_ruby_frame = true;
204
+ raw_locations[i].iseq = (VALUE) cfp->iseq;
205
+
206
+ if (cme) {
207
+ raw_locations[i].callable_method_entry = (VALUE) cme;
208
+ raw_locations[i].vm_method_type = cme->def->type;
209
+ }
210
+
211
+ if (!(cme && cme->def->type == VM_METHOD_TYPE_ISEQ)) {
212
+ // This comes from the original rb_profile_frames logic, which would only return the iseq when the cme
213
+ // type is not VM_METHOD_TYPE_ISEQ
214
+ raw_locations[i].should_use_iseq = true;
215
+ }
216
+
217
+ raw_locations[i].line_number = calc_lineno(cfp->iseq, cfp->pc);
218
+
219
+ i++;
220
+ } else {
221
+ if (cme && cme->def->type == VM_METHOD_TYPE_CFUNC) {
222
+ raw_locations[i].is_ruby_frame = false;
223
+ raw_locations[i].callable_method_entry = (VALUE) cme;
224
+ raw_locations[i].vm_method_type = cme->def->type;
225
+ raw_locations[i].line_number = 0;
226
+
227
+ i++;
228
+ }
229
+ }
230
+
231
+ cfp = RUBY_VM_PREVIOUS_CONTROL_FRAME(cfp);
232
+ }
233
+
234
+ return i;
235
+ }
236
+
237
+ int backtracie_rb_profile_frames(int limit, raw_location *raw_locations) {
238
+ return backtracie_rb_profile_frames_for_execution_context(GET_EC(), limit, raw_locations);
239
+ }
240
+
241
+ int backtracie_rb_profile_frames_for_thread(VALUE thread, int limit, raw_location *raw_locations) {
242
+ // In here we're assuming that what we got is really a Thread or its subclass. This assumption NEEDS to be verified by
243
+ // the caller, otherwise I see a segfault in your future.
244
+ rb_thread_t *thread_pointer = (rb_thread_t*) DATA_PTR(thread);
245
+
246
+ if (thread_pointer->to_kill || thread_pointer->status == THREAD_KILLED) return Qnil;
247
+
248
+ return backtracie_rb_profile_frames_for_execution_context(thread_pointer->ec, limit, raw_locations);
249
+ }
250
+
251
+ VALUE backtracie_called_id(raw_location *the_location) {
252
+ if (the_location->callable_method_entry == Qnil) return Qnil;
253
+
254
+ return ID2SYM(
255
+ ((rb_callable_method_entry_t *) the_location->callable_method_entry)
256
+ ->called_id
257
+ );
258
+ }
259
+
260
+ VALUE backtracie_defined_class(raw_location *the_location) {
261
+ if (the_location->callable_method_entry == Qnil) return Qnil;
262
+
263
+ return \
264
+ ((rb_callable_method_entry_t *) the_location->callable_method_entry)
265
+ ->defined_class;
266
+ }
267
+
268
+ VALUE backtracie_rb_vm_top_self() {
269
+ return GET_VM()->top_self;
270
+ }
271
+
272
+ bool backtracie_iseq_is_block(raw_location *the_location) {
273
+ if (the_location->iseq == Qnil) return false;
274
+
275
+ return ((rb_iseq_t *) the_location->iseq)->body->type == ISEQ_TYPE_BLOCK;
276
+ }
277
+
278
+ bool backtracie_iseq_is_eval(raw_location *the_location) {
279
+ if (the_location->iseq == Qnil) return false;
280
+
281
+ return ((rb_iseq_t *) the_location->iseq)->body->type == ISEQ_TYPE_EVAL;
282
+ }
283
+
284
+ VALUE backtracie_refinement_name(raw_location *the_location) {
285
+ VALUE defined_class = backtracie_defined_class(the_location);
286
+ if (defined_class == Qnil) return Qnil;
287
+
288
+ VALUE refinement_module = rb_class_of(defined_class);
289
+ if (!FL_TEST(refinement_module, RMODULE_IS_REFINEMENT)) return Qnil;
290
+
291
+ // The below bits are inspired by Ruby's rb_mod_to_s(VALUE)
292
+ ID id_refined_class;
293
+ CONST_ID(id_refined_class, "__refined_class__");
294
+ VALUE refined_class = rb_attr_get(refinement_module, id_refined_class);
295
+ if (refined_class == Qnil) return Qnil;
296
+
297
+ VALUE result = rb_inspect(refined_class);
298
+ rb_str_concat(result, rb_str_new2("$refinement@"));
299
+ ID id_defined_at;
300
+ CONST_ID(id_defined_at, "__defined_at__");
301
+ rb_str_concat(result, rb_inspect(rb_attr_get(refinement_module, id_defined_at)));
302
+
303
+ return result;
304
+ }
305
+
306
+ // For more details on the objective of this backport, see the comments on ruby_shards.h
307
+ // This is used for Ruby < 3.0.0
308
+ #ifdef CFUNC_FRAMES_BACKPORT_NEEDED
309
+
310
+ static const rb_callable_method_entry_t *
311
+ cframe(VALUE frame)
312
+ {
313
+ if (frame == Qnil) return NULL;
314
+
315
+ if (RB_TYPE_P(frame, T_IMEMO)) {
316
+ switch (imemo_type(frame)) {
317
+ case imemo_ment:
318
+ {
319
+ const rb_callable_method_entry_t *cme = (rb_callable_method_entry_t *)frame;
320
+ switch (cme->def->type) {
321
+ case VM_METHOD_TYPE_CFUNC:
322
+ return cme;
323
+ default:
324
+ return NULL;
325
+ }
326
+ }
327
+ default:
328
+ return NULL;
329
+ }
330
+ }
331
+
332
+ return NULL;
333
+ }
334
+
335
+ static const rb_iseq_t *
336
+ frame2iseq(VALUE frame)
337
+ {
338
+ if (frame == Qnil) return NULL;
339
+
340
+ if (RB_TYPE_P(frame, T_IMEMO)) {
341
+ switch (imemo_type(frame)) {
342
+ case imemo_iseq:
343
+ return (const rb_iseq_t *)frame;
344
+ case imemo_ment:
345
+ {
346
+ const rb_callable_method_entry_t *cme = (rb_callable_method_entry_t *)frame;
347
+ switch (cme->def->type) {
348
+ case VM_METHOD_TYPE_ISEQ:
349
+ return cme->def->body.iseq.iseqptr;
350
+ default:
351
+ return NULL;
352
+ }
353
+ }
354
+ default:
355
+ break;
356
+ }
357
+ }
358
+ rb_bug("frame2iseq: unreachable");
359
+ }
360
+
361
+ VALUE
362
+ backported_rb_profile_frame_method_name(VALUE frame)
363
+ {
364
+ const rb_callable_method_entry_t *cme = cframe(frame);
365
+ if (cme) {
366
+ ID mid = cme->def->original_id;
367
+ return id2str(mid);
368
+ }
369
+ const rb_iseq_t *iseq = frame2iseq(frame);
370
+ return iseq ? rb_iseq_method_name(iseq) : Qnil;
371
+ }
372
+
373
+ #endif // CFUNC_FRAMES_BACKPORT_NEEDED
374
+
375
+ #ifdef CLASSPATH_BACKPORT_NEEDED
376
+ static VALUE
377
+ frame2klass(VALUE frame)
378
+ {
379
+ if (frame == Qnil) return Qnil;
380
+
381
+ if (RB_TYPE_P(frame, T_IMEMO)) {
382
+ const rb_callable_method_entry_t *cme = (rb_callable_method_entry_t *)frame;
383
+
384
+ if (imemo_type(frame) == imemo_ment) {
385
+ return cme->defined_class;
386
+ }
387
+ }
388
+ return Qnil;
389
+ }
390
+
391
+ VALUE
392
+ backported_rb_profile_frame_classpath(VALUE frame)
393
+ {
394
+ VALUE klass = frame2klass(frame);
395
+
396
+ if (klass && !NIL_P(klass)) {
397
+ if (RB_TYPE_P(klass, T_ICLASS)) {
398
+ klass = RBASIC(klass)->klass;
399
+ }
400
+ else if (FL_TEST(klass, FL_SINGLETON)) {
401
+ klass = rb_ivar_get(klass, id__attached__);
402
+ if (!RB_TYPE_P(klass, T_CLASS) && !RB_TYPE_P(klass, T_MODULE))
403
+ return rb_sprintf("#<%s:%p>", rb_class2name(rb_obj_class(klass)), (void*)klass);
404
+ }
405
+ return rb_class_path(klass);
406
+ }
407
+ else {
408
+ return Qnil;
409
+ }
410
+ }
411
+
412
+ // Oddly enough, this method is on debug.h BUT NOT on the MJIT header. Since I've had
413
+ // crashes when trying to combine the MJIT header with the regular Ruby headers, let's
414
+ // just supply the declaration for this function, as otherwise the build seems to fail
415
+ // on macOS in CI.
416
+ VALUE rb_profile_frame_singleton_method_p(VALUE frame);
417
+
418
+ static VALUE
419
+ qualified_method_name(VALUE frame, VALUE method_name)
420
+ {
421
+ if (method_name != Qnil) {
422
+ VALUE classpath = backported_rb_profile_frame_classpath(frame);
423
+ VALUE singleton_p = rb_profile_frame_singleton_method_p(frame);
424
+
425
+ if (classpath != Qnil) {
426
+ return rb_sprintf("%"PRIsVALUE"%s%"PRIsVALUE,
427
+ classpath, singleton_p == Qtrue ? "." : "#", method_name);
428
+ }
429
+ else {
430
+ return method_name;
431
+ }
432
+ }
433
+ else {
434
+ return Qnil;
435
+ }
436
+ }
437
+
438
+ VALUE
439
+ backported_rb_profile_frame_qualified_method_name(VALUE frame)
440
+ {
441
+ VALUE method_name = backported_rb_profile_frame_method_name(frame);
442
+
443
+ return qualified_method_name(frame, method_name);
444
+ }
445
+ #endif // CLASSPATH_BACKPORT_NEEDED
446
+
447
+ #endif // ifndef PRE_MJIT_RUBY
448
+
449
+ // -----------------------------------------------------------------------------
450
+
451
+ #ifdef PRE_MJIT_RUBY
452
+
453
+ #include <stdbool.h>
454
+
455
+ #include "ruby.h"
456
+
457
+ #include "ruby_shards.h"
458
+
459
+ typedef struct rb_backtrace_location_struct {
460
+ enum LOCATION_TYPE {
461
+ LOCATION_TYPE_ISEQ = 1,
462
+ LOCATION_TYPE_ISEQ_CALCED,
463
+ LOCATION_TYPE_CFUNC,
464
+ } type;
465
+
466
+ union {
467
+ struct {
468
+ const VALUE iseq; // Originally const rb_iseq_t *
469
+ union {
470
+ const VALUE *pc;
471
+ int lineno;
472
+ } lineno;
473
+ } iseq;
474
+ struct {
475
+ ID mid;
476
+ struct rb_backtrace_location_struct *prev_loc;
477
+ } cfunc;
478
+ } body;
479
+ } rb_backtrace_location_t;
480
+
481
+ struct valued_frame_info {
482
+ rb_backtrace_location_t *loc;
483
+ VALUE btobj;
484
+ };
485
+
486
+ // For Ruby < 2.6, we can't rely on the MJIT header. So we need to get... crafty. This alternative to
487
+ // backtracie_rb_profile_frames_for_execution_context(...) piggybacks on the actual
488
+ // Thread::Backtrace::Locations instances returned by Ruby's Thread#backtrace_locations, and then uses
489
+ // knowledge of the VM internal layouts to get the data we need out of them.
490
+ //
491
+ // Currently, this approach gets us a lot less information than the Ruby >= 2.6 approach (e.g. currently we
492
+ // get exactly the same as Thread::Backtrace::Locations offers), so it's a crappier replacement, but it does
493
+ // get us support for older Rubies so it's better than nothing (to check the differences in behavior, check which
494
+ // tests are disabled on < 2.6).
495
+ int backtracie_profile_frames_from_ruby_locations(
496
+ VALUE ruby_locations_array,
497
+ raw_location *raw_locations
498
+ ) {
499
+ Check_Type(ruby_locations_array, T_ARRAY);
500
+ int ruby_locations_array_size = RARRAY_LEN(ruby_locations_array);
501
+
502
+ for (int i = 0; i < ruby_locations_array_size; i++) {
503
+ // FIXME: We should validate that what we get out of the array is a Thread::Backtrace::Location instance
504
+ VALUE location = rb_ary_entry(ruby_locations_array, i);
505
+
506
+ // The leap of faith -- let's get into the data from the Location instance
507
+ struct valued_frame_info *location_payload = DATA_PTR(location);
508
+
509
+ if (location_payload->loc->type == LOCATION_TYPE_ISEQ) {
510
+ // Trigger calculation of the line number; this is calculated lazily when this method is invoked and
511
+ // it turns the location type from LOCATION_TYPE_ISEQ to LOCATION_TYPE_ISEQ_CALCED.
512
+ rb_funcall(location, rb_intern("lineno"), 0);
513
+
514
+ if (location_payload->loc->type == LOCATION_TYPE_ISEQ) {
515
+ rb_raise(rb_eRuntimeError, "Internal error: Calling #lineno didn't turn a location into a LOCATION_TYPE_ISEQ_CALCED");
516
+ }
517
+ }
518
+
519
+ if (location_payload->loc->type == LOCATION_TYPE_ISEQ || location_payload->loc->type == LOCATION_TYPE_ISEQ_CALCED) {
520
+ // Ruby frame
521
+ raw_locations[i].is_ruby_frame = true;
522
+ raw_locations[i].should_use_iseq = true;
523
+ raw_locations[i].should_use_cfunc_name = false;
524
+ raw_locations[i].vm_method_type = -1;
525
+ // FIXME: Poke MRI to always get the "calced" version, see location_lineno in vm_backtrace for the context
526
+ raw_locations[i].line_number =
527
+ location_payload->loc->type == LOCATION_TYPE_ISEQ_CALCED ?
528
+ location_payload->loc->body.iseq.lineno.lineno : 0;
529
+ raw_locations[i].iseq = (VALUE) location_payload->loc->body.iseq.iseq;
530
+ raw_locations[i].callable_method_entry = Qnil;
531
+ raw_locations[i].self = Qnil;
532
+ raw_locations[i].cfunc_name = Qnil;
533
+ } else {
534
+ // cfunc frame
535
+ raw_locations[i].is_ruby_frame = false;
536
+ raw_locations[i].should_use_iseq = false;
537
+ raw_locations[i].should_use_cfunc_name = true;
538
+ raw_locations[i].vm_method_type = -1;
539
+ raw_locations[i].line_number = 0;
540
+ raw_locations[i].iseq = Qnil;
541
+ raw_locations[i].callable_method_entry = Qnil;
542
+ raw_locations[i].self = Qnil;
543
+ raw_locations[i].cfunc_name = rb_id2str(location_payload->loc->body.cfunc.mid);
544
+ }
545
+ }
546
+
547
+ return ruby_locations_array_size;
548
+ }
549
+
550
+ VALUE backtracie_called_id(raw_location *the_location) {
551
+ // Not available on PRE_MJIT_RUBY
552
+ return Qnil;
553
+ }
554
+
555
+ VALUE backtracie_defined_class(raw_location *the_location) {
556
+ // Not available on PRE_MJIT_RUBY
557
+ return Qnil;
558
+ }
559
+
560
+ VALUE backtracie_rb_vm_top_self() {
561
+ // This is only used on gem initialization so... Let's be a bit lazy :)
562
+ return rb_eval_string("TOPLEVEL_BINDING.eval('self')");
563
+ }
564
+
565
+ bool backtracie_iseq_is_block(raw_location *the_location) {
566
+ // Not available on PRE_MJIT_RUBY
567
+ return false;
568
+ }
569
+
570
+ bool backtracie_iseq_is_eval(raw_location *the_location) {
571
+ // Not available on PRE_MJIT_RUBY
572
+ return false;
573
+ }
574
+
575
+ VALUE backported_rb_profile_frame_method_name(VALUE frame) {
576
+ // Not available on PRE_MJIT_RUBY
577
+ return Qnil;
578
+ }
579
+
580
+ VALUE backtracie_refinement_name(raw_location *the_location) {
581
+ // Not available on PRE_MJIT_RUBY
582
+ return Qnil;
583
+ }
584
+
585
+ #endif // ifdef PRE_MJIT_RUBY