thread_safety 0.1.2 → 0.1.3

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: da80797122c8f15667ea491b39d080c31230d1afd179f14c190ed046471635b6
4
- data.tar.gz: d3c8f576439d8956eb082105ef45659dcf3b1bb3284a122b7721f8f953222030
3
+ metadata.gz: 2a97ea1ef817cda7d227855e1922f4af2eff2162f8494ea68fc2743f0febf2d0
4
+ data.tar.gz: bb7b718805c670511e7009c0c08c53fad6f997913db6473ea6339606eefbe576
5
5
  SHA512:
6
- metadata.gz: 8946716cb9f51a90ae2e6b435ef89878867da71fa2fc202652669f8c0dcfc245231b9224e3f3d7a58afe51969bf79c3c77c375b875ee428818f601c4db0dceb4
7
- data.tar.gz: 2434d000df9d525bd3eabe0f8fcb7a455634e04719afcabc0190cec48291274952820d5e2a191d1b6c4059919e938f93c5859f281a4ec7a9d3b0fbff26be6a2d
6
+ metadata.gz: 3f05f018aaf6ece0dcc642b376fc9b91deba62b53575c1a096073e8b72d9dc73aa51d6f1d9ab808b48f1ff5b78ee51313d9fdaf9e557595767f668d43acfe775
7
+ data.tar.gz: a1103a44b093da5e15b85bbe6399fa23c46728d86b8907f12688337f06d74da1df35a167e11baf774f87b7d81ac56d9b7643feb447375e86f8b61ce89cd946de
data/.rubocop.yml CHANGED
@@ -1,8 +1,9 @@
1
- AllCops:
2
- TargetRubyVersion: 3.1
1
+ inherit_gem:
2
+ rubocop-shopify: rubocop.yml
3
3
 
4
- Style/StringLiterals:
5
- EnforcedStyle: double_quotes
4
+ AllCops:
5
+ SuggestExtensions: false
6
6
 
7
- Style/StringLiteralsInInterpolation:
8
- EnforcedStyle: double_quotes
7
+ Style/GlobalVars:
8
+ Exclude:
9
+ - ext/extconf_base.rb
data/README.md CHANGED
@@ -1,13 +1,13 @@
1
1
  # Thread Safety
2
2
 
3
- This gem plugs into [Ruby's Modular GC](https://github.com/ruby/ruby/blob/master/gc/README.md) feature to detect potential thread-safety issues through reporting cross-thread writes.
3
+ This gem plugs into [Ruby's Modular GC](https://github.com/ruby/ruby/blob/master/gc/README.md) feature to detect potential thread-safety issues through reporting cross-thread and cross-fiber writes.
4
4
 
5
5
  ## Usage
6
6
 
7
7
  To use the Thread Safety gem, follow these steps:
8
8
 
9
9
  1. Build Ruby with Modular GC enabled by [following the "Building Ruby with Modular GC" guide](https://github.com/ruby/ruby/blob/master/gc/README.md#building-ruby-with-modular-gc).
10
- - Since Modular GC is still an experimental feature, this gem does not yet support released versions of Ruby. You must use the development version of Ruby.
10
+ - Since Modular GC is still an experimental feature, this gem does not yet support released versions of Ruby. You must use the development version of Ruby.
11
11
  2. Install the gem.
12
12
  3. Implement a callback to report when a thread-safety issue is detected:
13
13
 
@@ -16,3 +16,70 @@ To use the Thread Safety gem, follow these steps:
16
16
  puts "Offense: #{offense.object} #{offense.backtrace[0].path}:#{offense.backtrace[0].lineno}"
17
17
  end
18
18
  ```
19
+ 4. Run your code with `thread_safety` in the command line.
20
+
21
+ For example, if you had the following script in `test.rb` with the `callback` from above:
22
+
23
+ ```ruby
24
+ obj = []
25
+ Thread.new do
26
+ obj << 1
27
+ end.join
28
+ ```
29
+
30
+ Then you could run it as such:
31
+
32
+ ```sh
33
+ $ thread_safety ruby test.rb
34
+ Offense: [] test.rb:9
35
+ ```
36
+
37
+ ## API
38
+
39
+ ### `ThreadSafety.callback = cb`
40
+
41
+ Sets the callback that is called when an offense is detected. `cb` must accept one argument `offense`, which is of type [`ThreadSafety::Offense`](#threadsafetyoffense).
42
+
43
+ Set `cb` to nil to turn Thread Safety off.
44
+
45
+ ### `ThreadSafety.enabled = enabled`
46
+
47
+ Sets Thread Safety to be enabled or disabled globally. Defaults to `false`.
48
+
49
+ ### `ThreadSafety.suppress_warnings {}`
50
+
51
+ Disables Thread Safety offense detection for the block. This is useful for silencing offenses that are false-positives.
52
+
53
+ For example:
54
+
55
+ ```ruby
56
+ obj = []
57
+ Thread.new do
58
+ obj << 1
59
+ ThreadSafety.suppress_warnings { obj << 2 }
60
+ obj << 3
61
+ end.join
62
+ ```
63
+
64
+ Outputs:
65
+
66
+ ```
67
+ $ thread_safety ruby script.rb
68
+ Offense: [] script.rb:9
69
+ Offense: [1, 2] script.rb:11
70
+ ```
71
+
72
+ ### `Thread#thread_safety_enabled = enabled` and `Fiber#thread_safety_enabled = enabled`
73
+
74
+ Sets Thread Safety to be enabled or disabled for a particular Thread or Fiber. This value will ignore the global value set by `ThreadSafety.enabled=`, so you can use this to implement allowlists or denylists.
75
+
76
+ ### `ThreadSafety::Offense`
77
+
78
+ Class for a warning. Contains the following fields:
79
+
80
+ - `object`: The object that is being written to.
81
+ - `backtrace`: An array containing the Ruby backtrace of the write.
82
+ - `created_fiber`: The Fiber object that created `object`.
83
+ - `created_thread`: The Thread object that created `object`.
84
+ - `access_thread`: The Thread object that wrote into `object`.
85
+ - `access_fiber`: The Fiber object that wrote into `object`.
data/Rakefile CHANGED
@@ -1,9 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # require "bundler/gem_tasks"
3
+ require "bundler/gem_tasks"
4
4
  require "minitest/test_task"
5
5
 
6
- Minitest::TestTask.create
6
+ class ThreadSafetyTestTask < Minitest::TestTask
7
+ def ruby(*args, **options, &block)
8
+ env = { "RUBY_GC_LIBRARY" => "thread_safety" }
9
+
10
+ if args.length > 1
11
+ sh(env, RUBY, *args, **options, &block)
12
+ else
13
+ sh(env, "#{RUBY} #{args.first}", **options, &block)
14
+ end
15
+ end
16
+ end
17
+
18
+ ThreadSafetyTestTask.create
7
19
 
8
20
  # require "rubocop/rake_task"
9
21
 
@@ -21,7 +33,7 @@ end)
21
33
 
22
34
  task :check_modular_gc do
23
35
  modular_gc_dir = RbConfig::CONFIG["modular_gc_dir"]
24
- fail "Not using Ruby with modular GC enabled" if !modular_gc_dir || modular_gc_dir.empty?
36
+ raise "Not using Ruby with modular GC enabled" if !modular_gc_dir || modular_gc_dir.empty?
25
37
  end
26
38
 
27
39
  Rake::ExtensionTask.new do |ext|
data/exe/thread_safety CHANGED
@@ -13,5 +13,6 @@ modular_gc_dir = RbConfig::CONFIG["modular_gc_dir"]
13
13
  raise "Not using Ruby with modular GC enabled" if !modular_gc_dir || modular_gc_dir.empty?
14
14
 
15
15
  ENV["RUBY_GC_LIBRARY"] = "thread_safety"
16
+ ENV["THREAD_SAFETY_ENABLE"] = "true"
16
17
 
17
18
  exec(*ARGV)
data/ext/extconf.rb CHANGED
@@ -2,14 +2,29 @@
2
2
 
3
3
  require_relative "extconf_base"
4
4
 
5
+ ruby_version = "#{RbConfig::CONFIG["MAJOR"]}.#{RbConfig::CONFIG["MINOR"]}"
6
+
7
+ gc_path = File.expand_path("gc-#{ruby_version}", __dir__)
8
+ $INCFLAGS.prepend("-I#{gc_path} ") # rubocop:disable Style/GlobalVars
9
+
10
+ # Apply patches to vendored GC files
11
+ patches_dir = File.join(gc_path, "patches")
12
+ if File.directory?(patches_dir)
13
+ Dir.glob(File.join(patches_dir, "*.patch")).sort.each do |patch_file|
14
+ Dir.chdir(gc_path) do
15
+ system("patch", "-p1", "-N", "--silent", "-i", patch_file) || true
16
+ end
17
+ end
18
+ end
19
+
5
20
  create_gc_makefile("thread_safety") do |makefile|
6
21
  [
7
22
  *makefile,
8
23
 
9
24
  <<~MAKEFILE,
10
- install: install-modular-gc-dir
11
- install-modular-gc-dir: install-so
12
- cp $(DLLIB) $(modular_gc_dir)
25
+ install: install-modular-gc-dir
26
+ install-modular-gc-dir: install-so
27
+ cp $(DLLIB) $(modular_gc_dir)
13
28
  MAKEFILE
14
29
  ]
15
30
  end
@@ -0,0 +1,220 @@
1
+ /*
2
+ * Vendored from Ruby v3_4_8: darray.h
3
+ *
4
+ * This file is vendored because the modular GC for Ruby 3.4 needs a darray.h
5
+ * that matches the Ruby 3.4 API. The ext/darray.h is from Ruby 4.0 and has
6
+ * different function names (_without_gc variants).
7
+ *
8
+ * This vendoring can be removed when Ruby 3.4 support is dropped and the
9
+ * minimum Ruby version is 4.0+.
10
+ *
11
+ * License: Ruby License (same as Ruby itself)
12
+ */
13
+
14
+ #ifndef RUBY_DARRAY_H
15
+ #define RUBY_DARRAY_H
16
+
17
+ #include <stdint.h>
18
+ #include <stddef.h>
19
+ #include <stdlib.h>
20
+
21
+ // Type for a dynamic array. Use to declare a dynamic array.
22
+ // It is a pointer so it fits in st_table nicely. Designed
23
+ // to be fairly type-safe.
24
+ //
25
+ // NULL is a valid empty dynamic array.
26
+ //
27
+ // Example:
28
+ // rb_darray(char) char_array = NULL;
29
+ // rb_darray_append(&char_array, 'e');
30
+ // printf("pushed %c\n", *rb_darray_ref(char_array, 0));
31
+ // rb_darray_free(char_array);
32
+ //
33
+ #define rb_darray(T) struct { rb_darray_meta_t meta; T data[]; } *
34
+
35
+ // Copy an element out of the array. Warning: not bounds checked.
36
+ //
37
+ // T rb_darray_get(rb_darray(T) ary, size_t idx);
38
+ //
39
+ #define rb_darray_get(ary, idx) ((ary)->data[(idx)])
40
+
41
+ // Assign to an element. Warning: not bounds checked.
42
+ //
43
+ // void rb_darray_set(rb_darray(T) ary, size_t idx, T element);
44
+ //
45
+ #define rb_darray_set(ary, idx, element) ((ary)->data[(idx)] = (element))
46
+
47
+ // Get a pointer to an element. Warning: not bounds checked.
48
+ //
49
+ // T *rb_darray_ref(rb_darray(T) ary, size_t idx);
50
+ //
51
+ #define rb_darray_ref(ary, idx) (&((ary)->data[(idx)]))
52
+
53
+ /* Copy a new element into the array. ptr_to_ary is evaluated multiple times.
54
+ *
55
+ * void rb_darray_append(rb_darray(T) *ptr_to_ary, T element);
56
+ */
57
+ #define rb_darray_append(ptr_to_ary, element) do { \
58
+ rb_darray_ensure_space((ptr_to_ary), \
59
+ sizeof(**(ptr_to_ary)), \
60
+ sizeof((*(ptr_to_ary))->data[0])); \
61
+ rb_darray_set(*(ptr_to_ary), \
62
+ (*(ptr_to_ary))->meta.size, \
63
+ (element)); \
64
+ (*(ptr_to_ary))->meta.size++; \
65
+ } while (0)
66
+
67
+ #define rb_darray_insert(ptr_to_ary, idx, element) do { \
68
+ rb_darray_ensure_space((ptr_to_ary), \
69
+ sizeof(**(ptr_to_ary)), \
70
+ sizeof((*(ptr_to_ary))->data[0])); \
71
+ MEMMOVE( \
72
+ rb_darray_ref(*(ptr_to_ary), idx + 1), \
73
+ rb_darray_ref(*(ptr_to_ary), idx), \
74
+ (*(ptr_to_ary))->data[0], \
75
+ rb_darray_size(*(ptr_to_ary)) - idx); \
76
+ rb_darray_set(*(ptr_to_ary), idx, element); \
77
+ (*(ptr_to_ary))->meta.size++; \
78
+ } while (0)
79
+
80
+ // Iterate over items of the array in a for loop
81
+ //
82
+ #define rb_darray_foreach(ary, idx_name, elem_ptr_var) \
83
+ for (size_t idx_name = 0; idx_name < rb_darray_size(ary) && ((elem_ptr_var) = rb_darray_ref(ary, idx_name)); ++idx_name)
84
+
85
+ // Iterate over valid indices in the array in a for loop
86
+ //
87
+ #define rb_darray_for(ary, idx_name) \
88
+ for (size_t idx_name = 0; idx_name < rb_darray_size(ary); ++idx_name)
89
+
90
+ /* Make a dynamic array of a certain size. All bytes backing the elements are set to zero.
91
+ * Return 1 on success and 0 on failure.
92
+ *
93
+ * Note that NULL is a valid empty dynamic array.
94
+ *
95
+ * void rb_darray_make(rb_darray(T) *ptr_to_ary, size_t size);
96
+ */
97
+ #define rb_darray_make(ptr_to_ary, size) \
98
+ rb_darray_make_impl((ptr_to_ary), size, sizeof(**(ptr_to_ary)), sizeof((*(ptr_to_ary))->data[0]))
99
+
100
+ /* Resize the darray to a new capacity. The new capacity must be greater than
101
+ * or equal to the size of the darray.
102
+ *
103
+ * void rb_darray_resize_capa(rb_darray(T) *ptr_to_ary, size_t capa);
104
+ */
105
+ #define rb_darray_resize_capa(ptr_to_ary, capa) \
106
+ rb_darray_resize_capa_impl((ptr_to_ary), capa, sizeof(**(ptr_to_ary)), sizeof((*(ptr_to_ary))->data[0]))
107
+
108
+ #define rb_darray_data_ptr(ary) ((ary)->data)
109
+
110
+ typedef struct rb_darray_meta {
111
+ size_t size;
112
+ size_t capa;
113
+ } rb_darray_meta_t;
114
+
115
+ /* Set the size of the array to zero without freeing the backing memory.
116
+ * Allows reusing the same array. */
117
+ static inline void
118
+ rb_darray_clear(void *ary)
119
+ {
120
+ rb_darray_meta_t *meta = ary;
121
+ if (meta) {
122
+ meta->size = 0;
123
+ }
124
+ }
125
+
126
+ // Get the size of the dynamic array.
127
+ //
128
+ static inline size_t
129
+ rb_darray_size(const void *ary)
130
+ {
131
+ const rb_darray_meta_t *meta = ary;
132
+ return meta ? meta->size : 0;
133
+ }
134
+
135
+
136
+ static inline void
137
+ rb_darray_pop(void *ary, size_t count)
138
+ {
139
+ rb_darray_meta_t *meta = ary;
140
+ meta->size -= count;
141
+ }
142
+
143
+ // Get the capacity of the dynamic array.
144
+ //
145
+ static inline size_t
146
+ rb_darray_capa(const void *ary)
147
+ {
148
+ const rb_darray_meta_t *meta = ary;
149
+ return meta ? meta->capa : 0;
150
+ }
151
+
152
+ /* Free the dynamic array. */
153
+ static inline void
154
+ rb_darray_free(void *ary)
155
+ {
156
+ xfree(ary);
157
+ }
158
+
159
+ /* Internal function. Resizes the capacity of a darray. The new capacity must
160
+ * be greater than or equal to the size of the darray. */
161
+ static inline void
162
+ rb_darray_resize_capa_impl(void *ptr_to_ary, size_t new_capa, size_t header_size, size_t element_size)
163
+ {
164
+ rb_darray_meta_t **ptr_to_ptr_to_meta = ptr_to_ary;
165
+ rb_darray_meta_t *meta = *ptr_to_ptr_to_meta;
166
+
167
+ rb_darray_meta_t *new_ary = xrealloc(meta, new_capa * element_size + header_size);
168
+
169
+ if (meta == NULL) {
170
+ /* First allocation. Initialize size. On subsequence allocations
171
+ * realloc takes care of carrying over the size. */
172
+ new_ary->size = 0;
173
+ }
174
+
175
+ RUBY_ASSERT(new_ary->size <= new_capa);
176
+
177
+ new_ary->capa = new_capa;
178
+
179
+ // We don't have access to the type of the dynamic array in function context.
180
+ // Write out result with memcpy to avoid strict aliasing issue.
181
+ memcpy(ptr_to_ary, &new_ary, sizeof(new_ary));
182
+ }
183
+
184
+ // Internal function
185
+ // Ensure there is space for one more element.
186
+ // Note: header_size can be bigger than sizeof(rb_darray_meta_t) when T is __int128_t, for example.
187
+ static inline void
188
+ rb_darray_ensure_space(void *ptr_to_ary, size_t header_size, size_t element_size)
189
+ {
190
+ rb_darray_meta_t **ptr_to_ptr_to_meta = ptr_to_ary;
191
+ rb_darray_meta_t *meta = *ptr_to_ptr_to_meta;
192
+ size_t current_capa = rb_darray_capa(meta);
193
+ if (rb_darray_size(meta) < current_capa) return;
194
+
195
+ // Double the capacity
196
+ size_t new_capa = current_capa == 0 ? 1 : current_capa * 2;
197
+
198
+ rb_darray_resize_capa_impl(ptr_to_ary, new_capa, header_size, element_size);
199
+ }
200
+
201
+ static inline void
202
+ rb_darray_make_impl(void *ptr_to_ary, size_t array_size, size_t header_size, size_t element_size)
203
+ {
204
+ rb_darray_meta_t **ptr_to_ptr_to_meta = ptr_to_ary;
205
+ if (array_size == 0) {
206
+ *ptr_to_ptr_to_meta = NULL;
207
+ return;
208
+ }
209
+
210
+ rb_darray_meta_t *meta = xcalloc(array_size * element_size + header_size, 1);
211
+
212
+ meta->size = array_size;
213
+ meta->capa = array_size;
214
+
215
+ // We don't have access to the type of the dynamic array in function context.
216
+ // Write out result with memcpy to avoid strict aliasing issue.
217
+ memcpy(ptr_to_ary, &meta, sizeof(meta));
218
+ }
219
+
220
+ #endif /* RUBY_DARRAY_H */