datadog 2.0.0.beta2 → 2.0.0.rc1

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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +57 -1
  3. data/ext/datadog_profiling_native_extension/NativeExtensionDesign.md +1 -1
  4. data/ext/datadog_profiling_native_extension/collectors_cpu_and_wall_time_worker.c +8 -20
  5. data/ext/datadog_profiling_native_extension/collectors_thread_context.c +18 -10
  6. data/ext/datadog_profiling_native_extension/crashtracker.c +108 -0
  7. data/ext/datadog_profiling_native_extension/extconf.rb +9 -23
  8. data/ext/datadog_profiling_native_extension/heap_recorder.c +38 -3
  9. data/ext/datadog_profiling_native_extension/heap_recorder.h +5 -0
  10. data/ext/datadog_profiling_native_extension/http_transport.c +0 -93
  11. data/ext/datadog_profiling_native_extension/libdatadog_helpers.c +86 -0
  12. data/ext/datadog_profiling_native_extension/libdatadog_helpers.h +4 -0
  13. data/ext/datadog_profiling_native_extension/native_extension_helpers.rb +2 -12
  14. data/ext/datadog_profiling_native_extension/private_vm_api_access.c +25 -86
  15. data/ext/datadog_profiling_native_extension/profiling.c +2 -0
  16. data/ext/datadog_profiling_native_extension/ruby_helpers.h +3 -5
  17. data/ext/datadog_profiling_native_extension/stack_recorder.c +156 -55
  18. data/lib/datadog/appsec/contrib/devise/tracking.rb +8 -0
  19. data/lib/datadog/core/configuration/settings.rb +10 -79
  20. data/lib/datadog/core/remote/client.rb +1 -5
  21. data/lib/datadog/core/remote/configuration/repository.rb +1 -1
  22. data/lib/datadog/core/remote/dispatcher.rb +3 -3
  23. data/lib/datadog/core/telemetry/emitter.rb +1 -1
  24. data/lib/datadog/core/telemetry/http/response.rb +4 -0
  25. data/lib/datadog/opentelemetry/sdk/span_processor.rb +18 -1
  26. data/lib/datadog/profiling/component.rb +26 -2
  27. data/lib/datadog/profiling/crashtracker.rb +91 -0
  28. data/lib/datadog/profiling/exporter.rb +6 -3
  29. data/lib/datadog/profiling/http_transport.rb +7 -11
  30. data/lib/datadog/profiling/profiler.rb +9 -2
  31. data/lib/datadog/profiling/stack_recorder.rb +6 -2
  32. data/lib/datadog/profiling.rb +1 -0
  33. data/lib/datadog/tracing/component.rb +5 -1
  34. data/lib/datadog/tracing/configuration/dynamic.rb +39 -1
  35. data/lib/datadog/tracing/configuration/settings.rb +1 -0
  36. data/lib/datadog/tracing/contrib/active_record/configuration/resolver.rb +1 -0
  37. data/lib/datadog/tracing/contrib/active_record/integration.rb +10 -0
  38. data/lib/datadog/tracing/contrib/configuration/resolver.rb +43 -0
  39. data/lib/datadog/tracing/contrib/trilogy/instrumentation.rb +1 -1
  40. data/lib/datadog/tracing/remote.rb +5 -1
  41. data/lib/datadog/tracing/sampling/ext.rb +5 -1
  42. data/lib/datadog/tracing/sampling/matcher.rb +60 -31
  43. data/lib/datadog/tracing/sampling/rule.rb +12 -5
  44. data/lib/datadog/tracing/sampling/rule_sampler.rb +17 -1
  45. data/lib/datadog/tracing/sampling/span/matcher.rb +13 -41
  46. data/lib/datadog/tracing/span_link.rb +12 -6
  47. data/lib/datadog/tracing/span_operation.rb +6 -4
  48. data/lib/datadog/version.rb +1 -1
  49. metadata +7 -5
@@ -2,6 +2,8 @@
2
2
 
3
3
  #include <ruby.h>
4
4
 
5
+ static VALUE log_failure_to_process_tag(VALUE err_details);
6
+
5
7
  const char *ruby_value_type_to_string(enum ruby_value_type type) {
6
8
  return ruby_value_type_to_char_slice(type).ptr;
7
9
  }
@@ -60,3 +62,87 @@ size_t read_ddogerr_string_and_drop(ddog_Error *error, char *string, size_t capa
60
62
  ddog_Error_drop(error);
61
63
  return error_msg_size;
62
64
  }
65
+
66
+ __attribute__((warn_unused_result))
67
+ ddog_prof_Endpoint endpoint_from(VALUE exporter_configuration) {
68
+ ENFORCE_TYPE(exporter_configuration, T_ARRAY);
69
+
70
+ VALUE exporter_working_mode = rb_ary_entry(exporter_configuration, 0);
71
+ ENFORCE_TYPE(exporter_working_mode, T_SYMBOL);
72
+ ID working_mode = SYM2ID(exporter_working_mode);
73
+
74
+ ID agentless_id = rb_intern("agentless");
75
+ ID agent_id = rb_intern("agent");
76
+
77
+ if (working_mode != agentless_id && working_mode != agent_id) {
78
+ rb_raise(rb_eArgError, "Failed to initialize transport: Unexpected working mode, expected :agentless or :agent");
79
+ }
80
+
81
+ if (working_mode == agentless_id) {
82
+ VALUE site = rb_ary_entry(exporter_configuration, 1);
83
+ VALUE api_key = rb_ary_entry(exporter_configuration, 2);
84
+ ENFORCE_TYPE(site, T_STRING);
85
+ ENFORCE_TYPE(api_key, T_STRING);
86
+
87
+ return ddog_prof_Endpoint_agentless(char_slice_from_ruby_string(site), char_slice_from_ruby_string(api_key));
88
+ } else { // agent_id
89
+ VALUE base_url = rb_ary_entry(exporter_configuration, 1);
90
+ ENFORCE_TYPE(base_url, T_STRING);
91
+
92
+ return ddog_prof_Endpoint_agent(char_slice_from_ruby_string(base_url));
93
+ }
94
+ }
95
+
96
+ __attribute__((warn_unused_result))
97
+ ddog_Vec_Tag convert_tags(VALUE tags_as_array) {
98
+ ENFORCE_TYPE(tags_as_array, T_ARRAY);
99
+
100
+ long tags_count = RARRAY_LEN(tags_as_array);
101
+ ddog_Vec_Tag tags = ddog_Vec_Tag_new();
102
+
103
+ for (long i = 0; i < tags_count; i++) {
104
+ VALUE name_value_pair = rb_ary_entry(tags_as_array, i);
105
+
106
+ if (!RB_TYPE_P(name_value_pair, T_ARRAY)) {
107
+ ddog_Vec_Tag_drop(tags);
108
+ ENFORCE_TYPE(name_value_pair, T_ARRAY);
109
+ }
110
+
111
+ // Note: We can index the array without checking its size first because rb_ary_entry returns Qnil if out of bounds
112
+ VALUE tag_name = rb_ary_entry(name_value_pair, 0);
113
+ VALUE tag_value = rb_ary_entry(name_value_pair, 1);
114
+
115
+ if (!(RB_TYPE_P(tag_name, T_STRING) && RB_TYPE_P(tag_value, T_STRING))) {
116
+ ddog_Vec_Tag_drop(tags);
117
+ ENFORCE_TYPE(tag_name, T_STRING);
118
+ ENFORCE_TYPE(tag_value, T_STRING);
119
+ }
120
+
121
+ ddog_Vec_Tag_PushResult push_result =
122
+ ddog_Vec_Tag_push(&tags, char_slice_from_ruby_string(tag_name), char_slice_from_ruby_string(tag_value));
123
+
124
+ if (push_result.tag == DDOG_VEC_TAG_PUSH_RESULT_ERR) {
125
+ // libdatadog validates tags and may catch invalid tags that ddtrace didn't actually catch.
126
+ // We warn users about such tags, and then just ignore them.
127
+
128
+ int exception_state;
129
+ rb_protect(log_failure_to_process_tag, get_error_details_and_drop(&push_result.err), &exception_state);
130
+
131
+ // Since we are calling into Ruby code, it may raise an exception. Ensure that dynamically-allocated tags
132
+ // get cleaned before propagating the exception.
133
+ if (exception_state) {
134
+ ddog_Vec_Tag_drop(tags);
135
+ rb_jump_tag(exception_state); // "Re-raise" exception
136
+ }
137
+ }
138
+ }
139
+
140
+ return tags;
141
+ }
142
+
143
+ static VALUE log_failure_to_process_tag(VALUE err_details) {
144
+ VALUE datadog_module = rb_const_get(rb_cObject, rb_intern("Datadog"));
145
+ VALUE logger = rb_funcall(datadog_module, rb_intern("logger"), 0);
146
+
147
+ return rb_funcall(logger, rb_intern("warn"), 1, rb_sprintf("Failed to add tag to profiling request: %"PRIsVALUE, err_details));
148
+ }
@@ -40,3 +40,7 @@ ddog_CharSlice ruby_value_type_to_char_slice(enum ruby_value_type type);
40
40
  inline static char* string_from_char_slice(ddog_CharSlice slice) {
41
41
  return ruby_strndup(slice.ptr, slice.len);
42
42
  }
43
+
44
+ ddog_prof_Endpoint endpoint_from(VALUE exporter_configuration);
45
+
46
+ ddog_Vec_Tag convert_tags(VALUE tags_as_array);
@@ -15,7 +15,7 @@ module Datadog
15
15
  # The MJIT header was introduced on 2.6 and removed on 3.3; for other Rubies we rely on debase-ruby_core_source
16
16
  CAN_USE_MJIT_HEADER = RUBY_VERSION.start_with?('2.6', '2.7', '3.0.', '3.1.', '3.2.')
17
17
 
18
- LIBDATADOG_VERSION = '~> 7.0.0.1.0'
18
+ LIBDATADOG_VERSION = '~> 9.0.0.1.0'
19
19
 
20
20
  def self.fail_install_if_missing_extension?
21
21
  ENV[ENV_FAIL_INSTALL_IF_MISSING_EXTENSION].to_s.strip.downcase == 'true'
@@ -86,7 +86,6 @@ module Datadog
86
86
  on_macos? ||
87
87
  on_unknown_os? ||
88
88
  on_unsupported_cpu_arch? ||
89
- on_unsupported_ruby_version? ||
90
89
  expected_to_use_mjit_but_mjit_is_disabled? ||
91
90
  libdatadog_not_available? ||
92
91
  libdatadog_not_usable?
@@ -166,7 +165,7 @@ module Datadog
166
165
 
167
166
  # Validation for this check is done in extconf.rb because it relies on mkmf
168
167
  PKG_CONFIG_IS_MISSING = explain_issue(
169
- #+-----------------------------------------------------------------------------+
168
+ # ----------------------------------------------------------------------------+
170
169
  'the `pkg-config` system tool is missing.',
171
170
  'This issue can usually be fixed by installing one of the following:',
172
171
  'the `pkg-config` package on Homebrew and Debian/Ubuntu-based Linux;',
@@ -260,15 +259,6 @@ module Datadog
260
259
  architecture_not_supported unless RUBY_PLATFORM.start_with?('x86_64', 'aarch64', 'arm64')
261
260
  end
262
261
 
263
- private_class_method def self.on_unsupported_ruby_version?
264
- ruby_version_not_supported = explain_issue(
265
- 'the profiler only supports Ruby 2.3 or newer.',
266
- suggested: UPGRADE_RUBY,
267
- )
268
-
269
- ruby_version_not_supported if RUBY_VERSION.start_with?('2.1.', '2.2.')
270
- end
271
-
272
262
  # On some Rubies, we require the mjit header to be present. If Ruby was installed without MJIT support, we also skip
273
263
  # building the extension.
274
264
  private_class_method def self.expected_to_use_mjit_but_mjit_is_disabled?
@@ -129,12 +129,7 @@ bool is_current_thread_holding_the_gvl(void) {
129
129
  }
130
130
  #else
131
131
  current_gvl_owner gvl_owner(void) {
132
- rb_vm_t *vm =
133
- #ifndef NO_GET_VM
134
- GET_VM();
135
- #else
136
- thread_struct_from_object(rb_thread_current())->vm;
137
- #endif
132
+ rb_vm_t *vm = GET_VM();
138
133
 
139
134
  // BIG Issue: Ruby < 2.6 did not have the owner field. The really nice thing about the owner field is that it's
140
135
  // "atomic" -- when a thread sets it, it "declares" two things in a single step
@@ -163,7 +158,7 @@ bool is_current_thread_holding_the_gvl(void) {
163
158
  //
164
159
  // Thus an incorrect `is_current_thread_holding_the_gvl` result may lead to issues inside `rb_postponed_job_register_one`.
165
160
  //
166
- // For this reason we currently do not enable the new Ruby profiler on Ruby 2.5 and below by default, and we print a
161
+ // For this reason we currently do not enable the new Ruby profiler on Ruby 2.5 by default, and we print a
167
162
  // warning when customers force-enable it.
168
163
  bool gvl_acquired = vm->gvl.acquired != 0;
169
164
  rb_thread_t *current_owner = vm->running_thread;
@@ -213,12 +208,7 @@ uint64_t native_thread_id_for(VALUE thread) {
213
208
  // Returns the stack depth by using the same approach as rb_profile_frames and backtrace_each: get the positions
214
209
  // of the end and current frame pointers and subtracting them.
215
210
  ptrdiff_t stack_depth_for(VALUE thread) {
216
- #ifndef USE_THREAD_INSTEAD_OF_EXECUTION_CONTEXT // Modern Rubies
217
- const rb_execution_context_t *ec = thread_struct_from_object(thread)->ec;
218
- #else // Ruby < 2.5
219
- const rb_thread_t *ec = thread_struct_from_object(thread);
220
- #endif
221
-
211
+ const rb_execution_context_t *ec = thread_struct_from_object(thread)->ec;
222
212
  const rb_control_frame_t *cfp = ec->cfp, *end_cfp = RUBY_VM_END_CONTROL_FRAME(ec);
223
213
 
224
214
  if (end_cfp == NULL) return 0;
@@ -253,12 +243,7 @@ void ddtrace_thread_list(VALUE result_array) {
253
243
  rb_ractor_t *current_ractor = ddtrace_get_ractor();
254
244
  ccan_list_for_each(&current_ractor->threads.set, thread, lt_node) {
255
245
  #else
256
- rb_vm_t *vm =
257
- #ifndef NO_GET_VM
258
- GET_VM();
259
- #else
260
- thread_struct_from_object(rb_thread_current())->vm;
261
- #endif
246
+ rb_vm_t *vm = GET_VM();
262
247
  list_for_each(&vm->living_threads, thread, vmlt_node) {
263
248
  #endif
264
249
  switch (thread->status) {
@@ -394,13 +379,6 @@ calc_lineno(const rb_iseq_t *iseq, const VALUE *pc)
394
379
  // * Add thread argument
395
380
  // * Add is_ruby_frame argument
396
381
  // * Removed `if (lines)` tests -- require/assume that like `buff`, `lines` is always specified
397
- // * Support Ruby < 2.5 by using rb_thread_t instead of rb_execution_context_t (which did not exist and was just
398
- // part of rb_thread_t)
399
- // * Support Ruby < 2.4 by using `RUBY_VM_NORMAL_ISEQ_P(cfp->iseq)` instead of `VM_FRAME_RUBYFRAME_P(cfp)`.
400
- // Given that the Ruby 2.3 version of `rb_profile_frames` did not support native methods and thus did not need this
401
- // check, how did I figure out what to replace it with? I did it by looking at other places in the VM code where the
402
- // code looks exactly the same but Ruby 2.4 uses `VM_FRAME_RUBYFRAME_P` whereas Ruby 2.3 used `RUBY_VM_NORMAL_ISEQ_P`.
403
- // Examples of these are `errinfo_place` in `eval.c`, `rb_vm_get_ruby_level_next_cfp` (among others) in `vm.c`, etc.
404
382
  // * Skip dummy frame that shows up in main thread
405
383
  // * Add `end_cfp == NULL` and `end_cfp <= cfp` safety checks. These are used in a bunch of places in
406
384
  // `vm_backtrace.c` (`backtrace_each`, `backtrace_size`, `rb_ec_partial_backtrace_object`) but are conspicuously
@@ -449,11 +427,7 @@ int ddtrace_rb_profile_frames(VALUE thread, int start, int limit, VALUE *buff, i
449
427
  // Modified from upstream: Instead of using `GET_EC` to collect info from the current thread,
450
428
  // support sampling any thread (including the current) passed as an argument
451
429
  rb_thread_t *th = thread_struct_from_object(thread);
452
- #ifndef USE_THREAD_INSTEAD_OF_EXECUTION_CONTEXT // Modern Rubies
453
- const rb_execution_context_t *ec = th->ec;
454
- #else // Ruby < 2.5
455
- const rb_thread_t *ec = th;
456
- #endif
430
+ const rb_execution_context_t *ec = th->ec;
457
431
  const rb_control_frame_t *cfp = ec->cfp, *end_cfp = RUBY_VM_END_CONTROL_FRAME(ec);
458
432
  #ifndef NO_JIT_RETURN
459
433
  const rb_control_frame_t *top = cfp;
@@ -499,11 +473,7 @@ int ddtrace_rb_profile_frames(VALUE thread, int start, int limit, VALUE *buff, i
499
473
  // rb_profile_frames does not do this check, but `backtrace_each` (`vm_backtrace.c`) does. This frame is not
500
474
  // exposed by the Ruby backtrace APIs and for now we want to match its behavior 1:1
501
475
  }
502
- #ifndef USE_ISEQ_P_INSTEAD_OF_RUBYFRAME_P // Modern Rubies
503
476
  else if (VM_FRAME_RUBYFRAME_P(cfp)) {
504
- #else // Ruby < 2.4
505
- else if (RUBY_VM_NORMAL_ISEQ_P(cfp->iseq)) {
506
- #endif
507
477
  if (start > 0) {
508
478
  start--;
509
479
  continue;
@@ -719,51 +689,27 @@ check_method_entry(VALUE obj, int can_be_svar)
719
689
  }
720
690
  }
721
691
 
722
- #ifndef USE_LEGACY_RB_VM_FRAME_METHOD_ENTRY
723
- // Taken from upstream vm_insnhelper.c at commit 5f10bd634fb6ae8f74a4ea730176233b0ca96954 (March 2022, Ruby 3.2 trunk)
724
- // Copyright (C) 2007 Koichi Sasada
725
- // to support our custom rb_profile_frames (see above)
726
- //
727
- // While older Rubies may have this function, the symbol is not exported which leads to dynamic loader issues, e.g.
728
- // `dyld: lazy symbol binding failed: Symbol not found: _rb_vm_frame_method_entry`.
729
- //
730
- // Modifications: None
731
- MJIT_STATIC const rb_callable_method_entry_t *
732
- rb_vm_frame_method_entry(const rb_control_frame_t *cfp)
733
- {
734
- const VALUE *ep = cfp->ep;
735
- rb_callable_method_entry_t *me;
736
-
737
- while (!VM_ENV_LOCAL_P(ep)) {
738
- if ((me = check_method_entry(ep[VM_ENV_DATA_INDEX_ME_CREF], FALSE)) != NULL) return me;
739
- ep = VM_ENV_PREV_EP(ep);
740
- }
741
-
742
- return check_method_entry(ep[VM_ENV_DATA_INDEX_ME_CREF], TRUE);
743
- }
744
- #else
745
- // Taken from upstream vm_insnhelper.c at commit 556e9f726e2b80f6088982c6b43abfe68bfad591 (October 2018, ruby_2_3 branch)
746
- // Copyright (C) 2007 Koichi Sasada
747
- // to support our custom rb_profile_frames (see above)
748
- //
749
- // Quite a few macros in this function changed after Ruby 2.3. Rather than trying to fix the Ruby 3.2 version to work
750
- // with 2.3 constants, I decided to import the Ruby 2.3 version.
751
- //
752
- // Modifications: None
753
- const rb_callable_method_entry_t *
754
- rb_vm_frame_method_entry(const rb_control_frame_t *cfp)
755
- {
756
- VALUE *ep = cfp->ep;
757
- rb_callable_method_entry_t *me;
692
+ // Taken from upstream vm_insnhelper.c at commit 5f10bd634fb6ae8f74a4ea730176233b0ca96954 (March 2022, Ruby 3.2 trunk)
693
+ // Copyright (C) 2007 Koichi Sasada
694
+ // to support our custom rb_profile_frames (see above)
695
+ //
696
+ // While older Rubies may have this function, the symbol is not exported which leads to dynamic loader issues, e.g.
697
+ // `dyld: lazy symbol binding failed: Symbol not found: _rb_vm_frame_method_entry`.
698
+ //
699
+ // Modifications: None
700
+ MJIT_STATIC const rb_callable_method_entry_t *
701
+ rb_vm_frame_method_entry(const rb_control_frame_t *cfp)
702
+ {
703
+ const VALUE *ep = cfp->ep;
704
+ rb_callable_method_entry_t *me;
758
705
 
759
- while (!VM_EP_LEP_P(ep)) {
760
- if ((me = check_method_entry(ep[-1], FALSE)) != NULL) return me;
761
- ep = VM_EP_PREV_EP(ep);
762
- }
706
+ while (!VM_ENV_LOCAL_P(ep)) {
707
+ if ((me = check_method_entry(ep[VM_ENV_DATA_INDEX_ME_CREF], FALSE)) != NULL) return me;
708
+ ep = VM_ENV_PREV_EP(ep);
709
+ }
763
710
 
764
- return check_method_entry(ep[-1], TRUE);
765
- }
766
- #endif // USE_LEGACY_RB_VM_FRAME_METHOD_ENTRY
711
+ return check_method_entry(ep[VM_ENV_DATA_INDEX_ME_CREF], TRUE);
712
+ }
767
713
  #endif // RUBY_MJIT_HEADER
768
714
 
769
715
  #ifndef NO_RACTORS
@@ -880,13 +826,6 @@ static inline int ddtrace_imemo_type(VALUE imemo) {
880
826
  // This is used to workaround a VM bug. See "handle_sampling_signal" in "collectors_cpu_and_wall_time_worker" for details.
881
827
  #ifdef NO_POSTPONED_TRIGGER
882
828
  void *objspace_ptr_for_gc_finalize_deferred_workaround(void) {
883
- rb_vm_t *vm =
884
- #ifndef NO_GET_VM // TODO: Inline GET_VM below once we drop support in dd-trace-rb 2.x for < Ruby 2.5
885
- GET_VM();
886
- #else
887
- thread_struct_from_object(rb_thread_current())->vm;
888
- #endif
889
-
890
- return vm->objspace;
829
+ return GET_VM()->objspace;
891
830
  }
892
831
  #endif
@@ -19,6 +19,7 @@ void collectors_dynamic_sampling_rate_init(VALUE profiling_module);
19
19
  void collectors_idle_sampling_helper_init(VALUE profiling_module);
20
20
  void collectors_stack_init(VALUE profiling_module);
21
21
  void collectors_thread_context_init(VALUE profiling_module);
22
+ void crashtracker_init(VALUE profiling_module);
22
23
  void http_transport_init(VALUE profiling_module);
23
24
  void stack_recorder_init(VALUE profiling_module);
24
25
 
@@ -53,6 +54,7 @@ void DDTRACE_EXPORT Init_datadog_profiling_native_extension(void) {
53
54
  collectors_idle_sampling_helper_init(profiling_module);
54
55
  collectors_stack_init(profiling_module);
55
56
  collectors_thread_context_init(profiling_module);
57
+ crashtracker_init(profiling_module);
56
58
  http_transport_init(profiling_module);
57
59
  stack_recorder_init(profiling_module);
58
60
 
@@ -16,11 +16,6 @@ static inline VALUE process_pending_interruptions(DDTRACE_UNUSED VALUE _) {
16
16
  return Qnil;
17
17
  }
18
18
 
19
- // RB_UNLIKELY is not supported on Ruby 2.3
20
- #ifndef RB_UNLIKELY
21
- #define RB_UNLIKELY(x) x
22
- #endif
23
-
24
19
  // Calls process_pending_interruptions BUT "rescues" any exceptions to be raised, returning them instead as
25
20
  // a non-zero `pending_exception`.
26
21
  //
@@ -82,6 +77,9 @@ NORETURN(
82
77
  #define ENFORCE_SUCCESS_HELPER(expression, have_gvl) \
83
78
  { int result_syserr_errno = expression; if (RB_UNLIKELY(result_syserr_errno)) raise_syserr(result_syserr_errno, have_gvl, ADD_QUOTES(expression), __FILE__, __LINE__, __func__); }
84
79
 
80
+ #define RUBY_NUM_OR_NIL(val, condition, conv) ((val condition) ? conv(val) : Qnil)
81
+ #define RUBY_AVG_OR_NIL(total, count) ((count == 0) ? Qnil : DBL2NUM(((double) total) / count))
82
+
85
83
  // Called by ENFORCE_SUCCESS_HELPER; should not be used directly
86
84
  NORETURN(void raise_syserr(
87
85
  int syserr_errno,