datadog 2.0.0.beta2 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +65 -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 +23 -1
  26. data/lib/datadog/opentelemetry/sdk/trace/span.rb +3 -1
  27. data/lib/datadog/profiling/component.rb +26 -2
  28. data/lib/datadog/profiling/crashtracker.rb +91 -0
  29. data/lib/datadog/profiling/exporter.rb +6 -3
  30. data/lib/datadog/profiling/http_transport.rb +7 -11
  31. data/lib/datadog/profiling/profiler.rb +9 -2
  32. data/lib/datadog/profiling/stack_recorder.rb +6 -2
  33. data/lib/datadog/profiling.rb +1 -0
  34. data/lib/datadog/tracing/component.rb +5 -1
  35. data/lib/datadog/tracing/configuration/dynamic.rb +39 -1
  36. data/lib/datadog/tracing/configuration/settings.rb +1 -0
  37. data/lib/datadog/tracing/contrib/active_record/configuration/resolver.rb +1 -0
  38. data/lib/datadog/tracing/contrib/active_record/integration.rb +10 -0
  39. data/lib/datadog/tracing/contrib/configuration/resolver.rb +43 -0
  40. data/lib/datadog/tracing/contrib/trilogy/instrumentation.rb +1 -1
  41. data/lib/datadog/tracing/correlation.rb +10 -6
  42. data/lib/datadog/tracing/remote.rb +5 -1
  43. data/lib/datadog/tracing/sampling/ext.rb +5 -1
  44. data/lib/datadog/tracing/sampling/matcher.rb +60 -31
  45. data/lib/datadog/tracing/sampling/rule.rb +12 -5
  46. data/lib/datadog/tracing/sampling/rule_sampler.rb +17 -1
  47. data/lib/datadog/tracing/sampling/span/matcher.rb +13 -41
  48. data/lib/datadog/tracing/span_link.rb +12 -6
  49. data/lib/datadog/tracing/span_operation.rb +6 -4
  50. data/lib/datadog/version.rb +1 -1
  51. metadata +7 -4
@@ -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,