Skip to content

Commit 6988d5a

Browse files
committed
Move into own extension
1 parent 1a73d22 commit 6988d5a

File tree

14 files changed

+413
-227
lines changed

14 files changed

+413
-227
lines changed

Rakefile

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,12 @@ namespace :spec do
229229
t.pattern = CORE_WITH_LIBDATADOG_API.join(', ')
230230
t.rspec_opts = args.to_a.join(' ')
231231
end.tap do |t|
232-
Rake::Task[t.name].enhance(["compile:libdatadog_api.#{RUBY_VERSION[/\d+.\d+/]}_#{RUBY_PLATFORM}"])
232+
Rake::Task[t.name].enhance(
233+
[
234+
"compile:libdatadog_api.#{RUBY_VERSION[/\d+.\d+/]}_#{RUBY_PLATFORM}",
235+
"compile:datadog_runtime_stacks.#{RUBY_VERSION[/\d+.\d+/]}_#{RUBY_PLATFORM}"
236+
]
237+
)
233238
end
234239
# rubocop:enable Style/MultilineBlockChain
235240

@@ -319,7 +324,7 @@ namespace :spec do
319324
rescue => e
320325
# Compilation failed (likely unsupported Ruby version) - tests will skip gracefully
321326
puts "Warning: libdatadog_api compilation failed: #{e.class}: #{e}"
322-
puts "DSM tests will be skipped for this Ruby version"
327+
puts 'DSM tests will be skipped for this Ruby version'
323328
end
324329

325330
DSM_ENABLED_LIBRARIES.each do |task_name|
@@ -500,6 +505,10 @@ NATIVE_EXTS = [
500505
ext.ext_dir = 'ext/libdatadog_api'
501506
end,
502507

508+
Rake::ExtensionTask.new("datadog_runtime_stacks.#{RUBY_VERSION[/\d+.\d+/]}_#{RUBY_PLATFORM}") do |ext|
509+
ext.ext_dir = 'ext/datadog_runtime_stacks'
510+
end,
511+
503512
Rake::ExtensionTask.new("datadog_profiling_native_extension.#{RUBY_VERSION}_#{RUBY_PLATFORM}") do |ext|
504513
ext.ext_dir = 'ext/datadog_profiling_native_extension'
505514
end,

datadog.gemspec

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ Gem::Specification.new do |spec|
8383

8484
spec.extensions = [
8585
'ext/datadog_profiling_native_extension/extconf.rb',
86+
'ext/datadog_runtime_stacks/extconf.rb',
8687
'ext/libdatadog_api/extconf.rb'
8788
]
8889
end
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
#include "datadog_ruby_common.h"
2+
3+
// IMPORTANT: Currently this file is copy-pasted between extensions. Make sure to update all versions when doing any change!
4+
5+
void raise_unexpected_type(VALUE value, const char *value_name, const char *type_name, const char *file, int line, const char* function_name) {
6+
rb_exc_raise(
7+
rb_exc_new_str(
8+
rb_eTypeError,
9+
rb_sprintf("wrong argument %"PRIsVALUE" for '%s' (expected a %s) at %s:%d:in `%s'",
10+
rb_inspect(value),
11+
value_name,
12+
type_name,
13+
file,
14+
line,
15+
function_name
16+
)
17+
)
18+
);
19+
}
20+
21+
VALUE datadog_gem_version(void) {
22+
VALUE ddtrace_module = rb_const_get(rb_cObject, rb_intern("Datadog"));
23+
ENFORCE_TYPE(ddtrace_module, T_MODULE);
24+
VALUE version_module = rb_const_get(ddtrace_module, rb_intern("VERSION"));
25+
ENFORCE_TYPE(version_module, T_MODULE);
26+
VALUE version_string = rb_const_get(version_module, rb_intern("STRING"));
27+
ENFORCE_TYPE(version_string, T_STRING);
28+
return version_string;
29+
}
30+
31+
static VALUE log_failure_to_process_tag(VALUE err_details) {
32+
return log_warning(rb_sprintf("Failed to convert tag: %"PRIsVALUE, err_details));
33+
}
34+
35+
__attribute__((warn_unused_result))
36+
ddog_Vec_Tag convert_tags(VALUE tags_as_array) {
37+
ENFORCE_TYPE(tags_as_array, T_ARRAY);
38+
39+
long tags_count = RARRAY_LEN(tags_as_array);
40+
ddog_Vec_Tag tags = ddog_Vec_Tag_new();
41+
42+
for (long i = 0; i < tags_count; i++) {
43+
VALUE name_value_pair = rb_ary_entry(tags_as_array, i);
44+
45+
if (!RB_TYPE_P(name_value_pair, T_ARRAY)) {
46+
ddog_Vec_Tag_drop(tags);
47+
ENFORCE_TYPE(name_value_pair, T_ARRAY);
48+
}
49+
50+
// Note: We can index the array without checking its size first because rb_ary_entry returns Qnil if out of bounds
51+
VALUE tag_name = rb_ary_entry(name_value_pair, 0);
52+
VALUE tag_value = rb_ary_entry(name_value_pair, 1);
53+
54+
if (!(RB_TYPE_P(tag_name, T_STRING) && RB_TYPE_P(tag_value, T_STRING))) {
55+
ddog_Vec_Tag_drop(tags);
56+
ENFORCE_TYPE(tag_name, T_STRING);
57+
ENFORCE_TYPE(tag_value, T_STRING);
58+
}
59+
60+
ddog_Vec_Tag_PushResult push_result =
61+
ddog_Vec_Tag_push(&tags, char_slice_from_ruby_string(tag_name), char_slice_from_ruby_string(tag_value));
62+
63+
if (push_result.tag == DDOG_VEC_TAG_PUSH_RESULT_ERR) {
64+
// libdatadog validates tags and may catch invalid tags that ddtrace didn't actually catch.
65+
// We warn users about such tags, and then just ignore them.
66+
67+
int exception_state;
68+
rb_protect(log_failure_to_process_tag, get_error_details_and_drop(&push_result.err), &exception_state);
69+
70+
// Since we are calling into Ruby code, it may raise an exception. Ensure that dynamically-allocated tags
71+
// get cleaned before propagating the exception.
72+
if (exception_state) {
73+
ddog_Vec_Tag_drop(tags);
74+
rb_jump_tag(exception_state); // "Re-raise" exception
75+
}
76+
}
77+
}
78+
79+
return tags;
80+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
#pragma once
2+
3+
// IMPORTANT: Currently this file is copy-pasted between extensions. Make sure to update all versions when doing any change!
4+
5+
#include <ruby.h>
6+
#include <datadog/common.h>
7+
8+
// Used to mark symbols to be exported to the outside of the extension.
9+
// Consider very carefully before tagging a function with this.
10+
#define DDTRACE_EXPORT __attribute__ ((visibility ("default")))
11+
12+
// Used to mark function arguments that are deliberately left unused
13+
#ifdef __GNUC__
14+
#define DDTRACE_UNUSED __attribute__((unused))
15+
#else
16+
#define DDTRACE_UNUSED
17+
#endif
18+
19+
#define ADD_QUOTES_HELPER(x) #x
20+
#define ADD_QUOTES(x) ADD_QUOTES_HELPER(x)
21+
22+
// Ruby has a Check_Type(value, type) that is roughly equivalent to this BUT Ruby's version is rather cryptic when it fails
23+
// e.g. "wrong argument type nil (expected String)". This is a replacement that prints more information to help debugging.
24+
#define ENFORCE_TYPE(value, type) \
25+
{ if (RB_UNLIKELY(!RB_TYPE_P(value, type))) raise_unexpected_type(value, ADD_QUOTES(value), ADD_QUOTES(type), __FILE__, __LINE__, __func__); }
26+
27+
#define ENFORCE_BOOLEAN(value) \
28+
{ if (RB_UNLIKELY(value != Qtrue && value != Qfalse)) raise_unexpected_type(value, ADD_QUOTES(value), "true or false", __FILE__, __LINE__, __func__); }
29+
30+
#define ENFORCE_TYPED_DATA(value, type) \
31+
{ if (RB_UNLIKELY(!rb_typeddata_is_kind_of(value, type))) raise_unexpected_type(value, ADD_QUOTES(value), "TypedData of type " ADD_QUOTES(type), __FILE__, __LINE__, __func__); }
32+
33+
NORETURN(void raise_unexpected_type(VALUE value, const char *value_name, const char *type_name, const char *file, int line, const char* function_name));
34+
35+
// Helper to retrieve Datadog::VERSION::STRING
36+
VALUE datadog_gem_version(void);
37+
38+
static inline ddog_CharSlice char_slice_from_ruby_string(VALUE string) {
39+
ENFORCE_TYPE(string, T_STRING);
40+
ddog_CharSlice char_slice = {.ptr = RSTRING_PTR(string), .len = RSTRING_LEN(string)};
41+
return char_slice;
42+
}
43+
44+
static inline ddog_CharSlice char_slice_from_cstr(const char *cstr) {
45+
if (cstr == NULL) {
46+
return (ddog_CharSlice){.ptr = NULL, .len = 0};
47+
}
48+
return (ddog_CharSlice){.ptr = cstr, .len = strlen(cstr)};
49+
}
50+
51+
static inline VALUE log_warning(VALUE warning) {
52+
VALUE datadog_module = rb_const_get(rb_cObject, rb_intern("Datadog"));
53+
VALUE logger = rb_funcall(datadog_module, rb_intern("logger"), 0);
54+
55+
return rb_funcall(logger, rb_intern("warn"), 1, warning);
56+
}
57+
58+
__attribute__((warn_unused_result))
59+
ddog_Vec_Tag convert_tags(VALUE tags_as_array);
60+
61+
static inline VALUE ruby_string_from_error(const ddog_Error *error) {
62+
ddog_CharSlice char_slice = ddog_Error_message(error);
63+
return rb_str_new(char_slice.ptr, char_slice.len);
64+
}
65+
66+
static inline VALUE get_error_details_and_drop(ddog_Error *error) {
67+
VALUE result = ruby_string_from_error(error);
68+
ddog_Error_drop(error);
69+
return result;
70+
}
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
# rubocop:disable Style/StderrPuts
2+
# rubocop:disable Style/GlobalVars
3+
4+
require 'rubygems'
5+
require_relative '../libdatadog_extconf_helpers'
6+
7+
def skip_building_extension!(reason)
8+
$stderr.puts(
9+
"WARN: Skipping build of datadog_runtime_stacks (#{reason}). Runtime stack capture will not be available."
10+
)
11+
12+
fail_install_if_missing_extension = ENV['DD_FAIL_INSTALL_IF_MISSING_EXTENSION'].to_s.strip.downcase == 'true'
13+
14+
if fail_install_if_missing_extension
15+
require 'mkmf'
16+
Logging.message("[datadog] Failure cause: #{reason}")
17+
else
18+
File.write('Makefile', 'all install clean: # dummy makefile that does nothing')
19+
end
20+
21+
exit
22+
end
23+
24+
if ENV['DD_NO_EXTENSION'].to_s.strip.downcase == 'true'
25+
skip_building_extension!('the `DD_NO_EXTENSION` environment variable is/was set to `true` during installation')
26+
end
27+
skip_building_extension!('current Ruby VM is not supported') if RUBY_ENGINE != 'ruby'
28+
skip_building_extension!('Microsoft Windows is not supported') if Gem.win_platform?
29+
30+
libdatadog_issue = Datadog::LibdatadogExtconfHelpers.load_libdatadog_or_get_issue
31+
skip_building_extension!("issue setting up `libdatadog` gem: #{libdatadog_issue}") if libdatadog_issue
32+
33+
require 'mkmf'
34+
35+
# Because we can't control what compiler versions our customers use, shipping with -Werror by default is a no-go.
36+
# But we can enable it in CI, so that we quickly spot any new warnings that just got introduced.
37+
append_cflags '-Werror' if ENV['DATADOG_GEM_CI'] == 'true'
38+
39+
# Older gcc releases may not default to C99 and we need to ask for this. This is also used:
40+
# * by upstream Ruby -- search for gnu99 in the codebase
41+
# * by msgpack, another datadog gem dependency
42+
# (https://github.com/msgpack/msgpack-ruby/blob/18ce08f6d612fe973843c366ac9a0b74c4e50599/ext/msgpack/extconf.rb#L8)
43+
append_cflags '-std=gnu99'
44+
45+
# Gets really noisy when we include the MJIT header, let's omit it (TODO: Use #pragma GCC diagnostic instead?)
46+
append_cflags "-Wno-unused-function"
47+
48+
# Allow defining variables at any point in a function
49+
append_cflags '-Wno-declaration-after-statement'
50+
51+
# If we forget to include a Ruby header, the function call may still appear to work, but then
52+
# cause a segfault later. Let's ensure that never happens.
53+
append_cflags '-Werror-implicit-function-declaration'
54+
55+
# The native extension is not intended to expose any symbols/functions for other native libraries to use;
56+
# the sole exception being `Init_datadog_runtime_stacks` which needs to be visible for Ruby to call it when
57+
# it `dlopen`s the library.
58+
#
59+
# By setting this compiler flag, we tell it to assume that everything is private unless explicitly stated.
60+
# For more details see https://gcc.gnu.org/wiki/Visibility
61+
append_cflags '-fvisibility=hidden'
62+
63+
# Avoid legacy C definitions
64+
append_cflags '-Wold-style-definition'
65+
66+
# Enable all other compiler warnings
67+
append_cflags '-Wall'
68+
append_cflags '-Wextra'
69+
70+
if ENV['DDTRACE_DEBUG'] == 'true'
71+
$defs << '-DDD_DEBUG'
72+
CONFIG['optflags'] = '-O0'
73+
CONFIG['debugflags'] = '-ggdb3'
74+
end
75+
76+
# If we got here, libdatadog is available and loaded
77+
ENV['PKG_CONFIG_PATH'] = "#{ENV["PKG_CONFIG_PATH"]}:#{Libdatadog.pkgconfig_folder}"
78+
Logging.message("[datadog] PKG_CONFIG_PATH set to #{ENV["PKG_CONFIG_PATH"].inspect}\n")
79+
$stderr.puts("Using libdatadog #{Libdatadog::VERSION} from #{Libdatadog.pkgconfig_folder}")
80+
81+
unless pkg_config('datadog_profiling_with_rpath')
82+
Logging.message("[datadog] Ruby detected the pkg-config command is #{$PKGCONFIG.inspect}\n")
83+
84+
if Datadog::LibdatadogExtconfHelpers.pkg_config_missing?
85+
skip_building_extension!('the `pkg-config` system tool is missing')
86+
else
87+
skip_building_extension!('there was a problem in setting up the `libdatadog` dependency')
88+
end
89+
end
90+
91+
# See comments on the helper methods being used for why we need to additionally set this.
92+
# The extremely excessive escaping around ORIGIN below seems to be correct and was determined after a lot of
93+
# experimentation. We need to get these special characters across a lot of tools untouched...
94+
extra_relative_rpaths = [
95+
Datadog::LibdatadogExtconfHelpers.libdatadog_folder_relative_to_native_lib_folder(current_folder: __dir__),
96+
*Datadog::LibdatadogExtconfHelpers.libdatadog_folder_relative_to_ruby_extensions_folders,
97+
]
98+
extra_relative_rpaths.each { |folder| $LDFLAGS += " -Wl,-rpath,$$$\\\\{ORIGIN\\}/#{folder.to_str}" }
99+
Logging.message("[datadog] After pkg-config $LDFLAGS were set to: #{$LDFLAGS.inspect}\n")
100+
101+
# Enable access to Ruby VM internal headers for runtime stack walking
102+
# Ruby version compatibility definitions
103+
104+
# On Ruby 3.5, we can't ask the object_id from IMEMOs (https://github.com/ruby/ruby/pull/13347)
105+
$defs << "-DNO_IMEMO_OBJECT_ID" unless RUBY_VERSION < "3.5"
106+
107+
# On Ruby 2.5 and 3.3, this symbol was not visible. It is on 2.6 to 3.2, as well as 3.4+
108+
$defs << "-DNO_RB_OBJ_INFO" if RUBY_VERSION.start_with?("2.5", "3.3")
109+
110+
# On older Rubies, M:N threads were not available
111+
$defs << "-DNO_MN_THREADS_AVAILABLE" if RUBY_VERSION < "3.3"
112+
113+
# On older Rubies, we did not need to include the ractor header (this was built into the MJIT header)
114+
$defs << "-DNO_RACTOR_HEADER_INCLUDE" if RUBY_VERSION < "3.3"
115+
116+
# On older Rubies, some of the Ractor internal APIs were directly accessible
117+
$defs << "-DUSE_RACTOR_INTERNAL_APIS_DIRECTLY" if RUBY_VERSION < "3.3"
118+
119+
# On older Rubies, the first_lineno inside a location was a VALUE and not a int (https://github.com/ruby/ruby/pull/6430)
120+
$defs << "-DNO_INT_FIRST_LINENO" if RUBY_VERSION < "3.2"
121+
122+
# On older Rubies, there are no Ractors
123+
$defs << "-DNO_RACTORS" if RUBY_VERSION < "3"
124+
125+
# On older Rubies, rb_imemo_name did not exist
126+
$defs << "-DNO_IMEMO_NAME" if RUBY_VERSION < "3"
127+
128+
# Tag the native extension library with the Ruby version and Ruby platform.
129+
# This makes it easier for development (avoids "oops I forgot to rebuild when I switched my Ruby") and ensures that
130+
# the wrong library is never loaded.
131+
# When requiring, we need to use the exact same string, including the version and the platform.
132+
EXTENSION_NAME = "datadog_runtime_stacks.#{RUBY_VERSION[/\d+.\d+/]}_#{RUBY_PLATFORM}".freeze
133+
134+
CAN_USE_MJIT_HEADER = RUBY_VERSION.start_with?("2.6", "2.7", "3.0.", "3.1.", "3.2.")
135+
136+
mjit_header_worked = false
137+
138+
if CAN_USE_MJIT_HEADER
139+
mjit_header_file_name = "rb_mjit_min_header-#{RUBY_VERSION}.h"
140+
141+
# Validate that the mjit header can actually be compiled on this system.
142+
original_common_headers = MakeMakefile::COMMON_HEADERS
143+
MakeMakefile::COMMON_HEADERS = "".freeze
144+
if have_macro("RUBY_MJIT_H", mjit_header_file_name)
145+
MakeMakefile::COMMON_HEADERS = original_common_headers
146+
147+
$defs << "-DRUBY_MJIT_HEADER='\"#{mjit_header_file_name}\"'"
148+
149+
# NOTE: This needs to come after all changes to $defs
150+
create_header
151+
152+
# Note: -Wunused-parameter flag is intentionally added here only after MJIT header validation
153+
append_cflags "-Wunused-parameter"
154+
155+
create_makefile(EXTENSION_NAME)
156+
mjit_header_worked = true
157+
else
158+
# MJIT header compilation failed, fallback to datadog-ruby_core_source
159+
$stderr.puts "MJIT header compilation failed, falling back to datadog-ruby_core_source"
160+
MakeMakefile::COMMON_HEADERS = original_common_headers
161+
end
162+
end
163+
164+
# The MJIT header was introduced on 2.6 and removed on 3.3; for other Rubies we rely on
165+
# the datadog-ruby_core_source gem to get access to private VM headers.
166+
# We also use it as a fallback when MJIT header compilation fails.
167+
unless mjit_header_worked
168+
create_header
169+
170+
require "datadog/ruby_core_source"
171+
dir_config("ruby") # allow user to pass in non-standard core include directory
172+
173+
# Workaround for mkmf issue with $CPPFLAGS
174+
Datadog::RubyCoreSource.define_singleton_method(:with_cppflags) do |newflags, &block|
175+
super("#{newflags} #{$CPPFLAGS}", &block)
176+
end
177+
178+
makefile_created = Datadog::RubyCoreSource
179+
.create_makefile_with_core(
180+
proc do
181+
headers_available =
182+
have_header("vm_core.h") &&
183+
have_header("iseq.h") &&
184+
(RUBY_VERSION < "3.3" || have_header("ractor_core.h"))
185+
186+
if headers_available
187+
append_cflags "-Wunused-parameter"
188+
end
189+
190+
headers_available
191+
end,
192+
EXTENSION_NAME
193+
)
194+
195+
unless makefile_created
196+
skip_building_extension!('required Ruby VM internal headers are not available for this Ruby version')
197+
end
198+
end
199+
200+
# rubocop:enable Style/GlobalVars
201+
# rubocop:enable Style/StderrPuts

0 commit comments

Comments
 (0)