rudy4 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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +114 -0
- data/LICENSE +21 -0
- data/Makefile +137 -0
- data/README.md +400 -0
- data/bin/rudy4 +6 -0
- data/examples/hello_gem/README.md +91 -0
- data/examples/hello_gem/ext/hello_gem/extconf.rb +75 -0
- data/examples/hello_gem/ext/hello_gem/hello_gem.d +122 -0
- data/examples/hello_gem/hello_gem.gemspec +35 -0
- data/examples/hello_gem/lib/hello_gem/version.rb +5 -0
- data/examples/hello_gem/lib/hello_gem.rb +32 -0
- data/examples/hello_gem/test/smoke.sh +81 -0
- data/experiments/exp_compact.d +178 -0
- data/experiments/exp_convert.d +93 -0
- data/experiments/exp_cross.d +144 -0
- data/experiments/exp_defargs.d +301 -0
- data/experiments/exp_exc.d +265 -0
- data/experiments/exp_gcstorm.d +169 -0
- data/experiments/exp_http.d +254 -0
- data/experiments/exp_nonregression.d +209 -0
- data/experiments/exp_objproto.d +123 -0
- data/experiments/exp_perf.d +81 -0
- data/experiments/exp_wrap.d +186 -0
- data/lib/rudy4/cli.rb +204 -0
- data/lib/rudy4/doctor.rb +295 -0
- data/lib/rudy4/error.rb +10 -0
- data/lib/rudy4/generator.rb +172 -0
- data/lib/rudy4/templates.rb +484 -0
- data/lib/rudy4/vendor.rb +36 -0
- data/lib/rudy4/version.rb +5 -0
- data/lib/rudy4.rb +16 -0
- data/scripts/abi_audit.sh +64 -0
- data/scripts/memcheck.sh +77 -0
- data/scripts/vendor_check.sh +33 -0
- data/scripts/vendor_sync.sh +25 -0
- data/src/dexitest.d +278 -0
- data/src/dextest.d +119 -0
- data/src/dtypetest.d +159 -0
- data/src/rudy/args.d +253 -0
- data/src/rudy/bind/value.d +306 -0
- data/src/rudy/convert.d +221 -0
- data/src/rudy/defs.d +440 -0
- data/src/rudy/objproto.d +282 -0
- data/src/rudy/typeddata.d +143 -0
- data/test/dextest_loader.rb +6 -0
- data/test/exitest.rb +113 -0
- data/test/rudy4_test.rb +291 -0
- data/test/run.rb +74 -0
- data/test/typedtest.rb +69 -0
- data/vendor/rudy/args.d +253 -0
- data/vendor/rudy/bind/value.d +306 -0
- data/vendor/rudy/convert.d +221 -0
- data/vendor/rudy/defs.d +440 -0
- data/vendor/rudy/objproto.d +282 -0
- data/vendor/rudy/typeddata.d +143 -0
- metadata +106 -0
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/// EXPERIMENT 6 — D <-> Ruby exception ping-pong torture.
|
|
2
|
+
///
|
|
3
|
+
/// Torture elements:
|
|
4
|
+
/// - rb_rescue / rb_ensure / rb_protect nested inside each other, with
|
|
5
|
+
/// bodies that call BACK into Ruby (rb_funcall) and raise from both sides
|
|
6
|
+
/// of the boundary in every combination.
|
|
7
|
+
/// - Mutual recursion Ruby -> D -> Ruby -> D ... 50 levels deep, unwinding
|
|
8
|
+
/// through raises at the deepest level: longjmp must cleanly skip D
|
|
9
|
+
/// frames without corrupting D state.
|
|
10
|
+
/// - State checks after each unwind: module vars survive, D-GC memory
|
|
11
|
+
/// allocated before the raise is still reachable (no collection of live
|
|
12
|
+
/// objects due to the longjmp), and the Ruby exception object surfacing
|
|
13
|
+
/// in Ruby is the SAME object raised inside the D call (identity check).
|
|
14
|
+
/// - rb_protect with nested rb_protect: inner raise caught by inner
|
|
15
|
+
/// protect, outer protect sees state == 0.
|
|
16
|
+
/// - rb_ensure: the ensure callback runs even when the body raises through
|
|
17
|
+
/// two nested rescues.
|
|
18
|
+
///
|
|
19
|
+
/// Layout:
|
|
20
|
+
/// DExc.echo_or_raise(n) -> calls Ruby method (n times recursively via
|
|
21
|
+
/// Ruby-level recursion into this function)
|
|
22
|
+
/// DExc.rescue_case -> rb_rescue around a raising body
|
|
23
|
+
/// DExc.ensure_case -> rb_ensure: body raises, ensure counts
|
|
24
|
+
/// DExc.protect_case -> rb_protect with nested protect inside
|
|
25
|
+
/// DExc.deep_ping_pong -> the recursion torture (see test)
|
|
26
|
+
/// DExc.state -> survival checks after unwinds
|
|
27
|
+
/// DExc.identical_exc -> raise inside rb_protect, re-raise SAME VALUE
|
|
28
|
+
|
|
29
|
+
module exp_exc;
|
|
30
|
+
|
|
31
|
+
import rudy.bind.value;
|
|
32
|
+
import rudy.convert;
|
|
33
|
+
|
|
34
|
+
import core.stdc.stdlib : malloc, free;
|
|
35
|
+
|
|
36
|
+
private:
|
|
37
|
+
|
|
38
|
+
// All module state __gshared: D module vars are thread-local by default
|
|
39
|
+
// (torture finding #1) — Ruby threads are OS pthreads.
|
|
40
|
+
__gshared:
|
|
41
|
+
long calls_made; // every entry into a D function
|
|
42
|
+
long ensures_run; // every rb_ensure ensure callback
|
|
43
|
+
long rescues_run; // every rb_rescue rescue callback
|
|
44
|
+
long protects_caught; // every rb_protect that observed state != 0
|
|
45
|
+
long max_depth_seen; // deepest recursion observed (mutual D<->Ruby)
|
|
46
|
+
VALUE exc_kept; // exception VALUE held across a longjmp
|
|
47
|
+
VALUE exc_reseen; // exception VALUE re-observed after re-raise
|
|
48
|
+
int ping_pong_failed; // set when deep recursion returns wrong value
|
|
49
|
+
|
|
50
|
+
// ---------------------------------------------------------- helpers
|
|
51
|
+
|
|
52
|
+
extern (C) VALUE keep_exc(VALUE exc)
|
|
53
|
+
{
|
|
54
|
+
// Store the pending VALUE (from rb_errinfo after a protect catch).
|
|
55
|
+
exc_kept = exc;
|
|
56
|
+
return Qnil_;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
extern (C) void note_exception(VALUE exc) { exc_kept = exc; }
|
|
60
|
+
|
|
61
|
+
// ---------------------------------------------------------- echo_or_raise
|
|
62
|
+
// Ruby test calls: DExc.echo_or_raise(depth)
|
|
63
|
+
// - if depth > 0: call Ruby-side DExc.ping_pong(depth - 1) — Ruby method
|
|
64
|
+
// defined in the test file calls DExc.echo_or_raise back. Mutual
|
|
65
|
+
// recursion through the boundary.
|
|
66
|
+
// - if depth == 0: raise Ruby RuntimeError (longjmp from D code).
|
|
67
|
+
// - if depth < 0: return a string; no raise (clean path).
|
|
68
|
+
|
|
69
|
+
extern (C) VALUE echo_or_raise(VALUE self, VALUE depth)
|
|
70
|
+
{
|
|
71
|
+
calls_made += 1;
|
|
72
|
+
long d = d_type!long(depth);
|
|
73
|
+
if (d > 0)
|
|
74
|
+
{
|
|
75
|
+
// record depth for the max-depth assertion
|
|
76
|
+
if (d > max_depth_seen) max_depth_seen = d;
|
|
77
|
+
// Mutual recursion: D -> Ruby method -> D again.
|
|
78
|
+
return rb_funcall(rb_mKernel, rb_intern("DExc_ping_pong"), 1, rb_int2inum(d - 1));
|
|
79
|
+
}
|
|
80
|
+
else if (d == 0)
|
|
81
|
+
{
|
|
82
|
+
// raise from D: longjmp straight out of this D frame (never returns)
|
|
83
|
+
rb_raise(rb_eRuntimeError, "bottom of recursion");
|
|
84
|
+
assert(0);
|
|
85
|
+
}
|
|
86
|
+
else
|
|
87
|
+
{
|
|
88
|
+
return rb_str_new_cstr("clean");
|
|
89
|
+
}
|
|
90
|
+
assert(0); // rb_raise is noreturn; silence D's no-return analysis
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ---------------------------------------------------------- rescue_case
|
|
94
|
+
// rb_rescue around a Ruby-method body that raises. The rescue callback
|
|
95
|
+
// runs with the exception VALUE in hand — we store it and return a marker.
|
|
96
|
+
|
|
97
|
+
extern (C) VALUE raising_body(VALUE data) { return rb_funcall(rb_mKernel, rb_intern("DExc_raise"), 0); }
|
|
98
|
+
extern (C) VALUE rescue_body(VALUE data, VALUE exc)
|
|
99
|
+
{
|
|
100
|
+
rescues_run += 1;
|
|
101
|
+
note_exception(exc);
|
|
102
|
+
return rb_str_new_cstr("rescued");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
extern (C) VALUE rescue_case(VALUE self)
|
|
106
|
+
{
|
|
107
|
+
calls_made += 1;
|
|
108
|
+
return rb_rescue(&raising_body, Qnil_, &rescue_body, Qnil_);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ---------------------------------------------------------- ensure_case
|
|
112
|
+
// rb_ensure: body raises; the ensure callback must still run (its counter
|
|
113
|
+
// is the assertion) and the exception must propagate to the Ruby caller.
|
|
114
|
+
|
|
115
|
+
extern (C) VALUE ensure_ensure(VALUE data)
|
|
116
|
+
{
|
|
117
|
+
ensures_run += 1;
|
|
118
|
+
return Qnil_;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
extern (C) VALUE ensure_case(VALUE self)
|
|
122
|
+
{
|
|
123
|
+
calls_made += 1;
|
|
124
|
+
return rb_ensure(&raising_body, Qnil_, &ensure_ensure, Qnil_);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ---------------------------------------------------------- protect_case
|
|
128
|
+
// Nested rb_protect: the inner protect catches an inner raise; the outer
|
|
129
|
+
// protect runs a clean body and must see state == 0. If the inner raise
|
|
130
|
+
// leaked outward, the whole VM would die (this actually happened with
|
|
131
|
+
// early def! trampolines — chunk-1 finding).
|
|
132
|
+
|
|
133
|
+
extern (C) VALUE inner_protect_body(VALUE data) { return rb_funcall(rb_mKernel, rb_intern("DExc_raise"), 0); }
|
|
134
|
+
|
|
135
|
+
extern (C) VALUE inner_protect(VALUE data)
|
|
136
|
+
{
|
|
137
|
+
int state = 0;
|
|
138
|
+
VALUE v = rb_protect(&inner_protect_body, Qnil_, &state);
|
|
139
|
+
if (state != 0)
|
|
140
|
+
{
|
|
141
|
+
protects_caught += 1;
|
|
142
|
+
// clear the pending exception: we HANDLED it (else the next Ruby
|
|
143
|
+
// API call spuriously raises at an arbitrary point)
|
|
144
|
+
rb_set_errinfo(Qnil_);
|
|
145
|
+
return rb_str_new_cstr("inner caught");
|
|
146
|
+
}
|
|
147
|
+
return v;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
extern (C) VALUE protect_body(VALUE data) { return rb_funcall(rb_mKernel, rb_intern("DExc_inner"), 0); }
|
|
151
|
+
|
|
152
|
+
// The Ruby-side stepper for the outer protect body calls THIS (registered
|
|
153
|
+
// as DExc.protect_inner): nested protect, raise contained at the inner.
|
|
154
|
+
extern (C) VALUE protect_inner(VALUE self)
|
|
155
|
+
{
|
|
156
|
+
return inner_protect(Qnil_);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
extern (C) VALUE protect_case(VALUE self)
|
|
160
|
+
{
|
|
161
|
+
calls_made += 1;
|
|
162
|
+
int state = 0;
|
|
163
|
+
VALUE v = rb_protect(&protect_body, Qnil_, &state);
|
|
164
|
+
if (state != 0)
|
|
165
|
+
{
|
|
166
|
+
protects_caught += 1;
|
|
167
|
+
rb_set_errinfo(Qnil_);
|
|
168
|
+
return rb_str_new_cstr("outer caught unexpectedly");
|
|
169
|
+
}
|
|
170
|
+
return v;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ---------------------------------------------------------- deep ping-pong
|
|
174
|
+
// Called from Ruby with a big depth and a block: recurses D -> Ruby -> D
|
|
175
|
+
// with the block called at every level, then raises at the bottom. The
|
|
176
|
+
// Ruby rescue wraps the whole thing; we assert the unwind depth bookkeeping.
|
|
177
|
+
|
|
178
|
+
extern (C) VALUE deep_ping_pong(VALUE self, VALUE depth)
|
|
179
|
+
{
|
|
180
|
+
calls_made += 1;
|
|
181
|
+
long d = d_type!long(depth);
|
|
182
|
+
if (d > max_depth_seen) max_depth_seen = d;
|
|
183
|
+
if (d == 0)
|
|
184
|
+
{
|
|
185
|
+
rb_raise(rb_eRuntimeError, "deep bottom");
|
|
186
|
+
assert(0);
|
|
187
|
+
}
|
|
188
|
+
// call the Ruby-side stepper (defined in the test file); a dedicated
|
|
189
|
+
// name so the deep test does not route through echo_or_raise's stepper
|
|
190
|
+
return rb_funcall(rb_mKernel, rb_intern("DExc_deep_ping_pong"), 1, rb_int2inum(d - 1));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ---------------------------------------------------------- identical_exc
|
|
194
|
+
// Prove that the exception object raised inside a protected body is the
|
|
195
|
+
// SAME object that later appears in Ruby's rescue: identity via rb_obj_id.
|
|
196
|
+
// (also exercises rb_exc_raise: D re-raises the SAME exception VALUE after
|
|
197
|
+
// the protect catch — re-raising with rb_raise would build a new exc.)
|
|
198
|
+
|
|
199
|
+
extern (C) VALUE bomb_body(VALUE data) { return rb_funcall(rb_mKernel, rb_intern("DExc_raise"), 0); }
|
|
200
|
+
|
|
201
|
+
extern (C) VALUE identical_exc(VALUE self)
|
|
202
|
+
{
|
|
203
|
+
calls_made += 1;
|
|
204
|
+
int state = 0;
|
|
205
|
+
VALUE v = rb_protect(&bomb_body, Qnil_, &state);
|
|
206
|
+
if (state != 0)
|
|
207
|
+
{
|
|
208
|
+
protects_caught += 1;
|
|
209
|
+
exc_reseen = rb_errinfo();
|
|
210
|
+
rb_set_errinfo(Qnil_);
|
|
211
|
+
// return the exception itself; the Ruby test compares object ids
|
|
212
|
+
// after catching a re-raise of this same VALUE via rb_exc_raise
|
|
213
|
+
return exc_reseen;
|
|
214
|
+
}
|
|
215
|
+
return v;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
extern (C) VALUE reraise(VALUE self, VALUE exc)
|
|
219
|
+
{
|
|
220
|
+
calls_made += 1;
|
|
221
|
+
exc_reseen = exc;
|
|
222
|
+
rb_exc_raise(exc); // never returns
|
|
223
|
+
assert(0);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ---------------------------------------------------------- state
|
|
227
|
+
|
|
228
|
+
extern (C) VALUE get_calls(VALUE self) { return rb_int2inum(calls_made); }
|
|
229
|
+
extern (C) VALUE get_ensures(VALUE self) { return rb_int2inum(ensures_run); }
|
|
230
|
+
extern (C) VALUE get_rescues(VALUE self) { return rb_int2inum(rescues_run); }
|
|
231
|
+
extern (C) VALUE get_protects(VALUE self) { return rb_int2inum(protects_caught); }
|
|
232
|
+
extern (C) VALUE get_max_depth(VALUE self) { return rb_int2inum(max_depth_seen); }
|
|
233
|
+
extern (C) VALUE get_ping_fail(VALUE self) { return ping_pong_failed ? Qtrue_ : Qfalse_; }
|
|
234
|
+
extern (C) VALUE get_exc_kept(VALUE self) { return exc_kept; }
|
|
235
|
+
extern (C) VALUE get_exc_reseen(VALUE self) { return exc_reseen; }
|
|
236
|
+
|
|
237
|
+
// ---------------------------------------------------------- init
|
|
238
|
+
|
|
239
|
+
pragma(mangle, "Init_exp_exc")
|
|
240
|
+
extern (C) void Init_exp_exc()
|
|
241
|
+
{
|
|
242
|
+
// The two __gshared VALUE slots hold exceptions across D calls; Ruby's
|
|
243
|
+
// GC would otherwise be free to collect them mid-experiment (a VALUE
|
|
244
|
+
// stored only in a D global is invisible to the collector).
|
|
245
|
+
rb_gc_register_address(&exc_kept);
|
|
246
|
+
rb_gc_register_address(&exc_reseen);
|
|
247
|
+
|
|
248
|
+
VALUE m = rb_define_module("DExc");
|
|
249
|
+
rb_define_module_function(m, "echo_or_raise", cast(AnyMethod) &echo_or_raise, 1);
|
|
250
|
+
rb_define_module_function(m, "rescue_case", cast(AnyMethod) &rescue_case, 0);
|
|
251
|
+
rb_define_module_function(m, "ensure_case", cast(AnyMethod) &ensure_case, 0);
|
|
252
|
+
rb_define_module_function(m, "protect_case", cast(AnyMethod) &protect_case, 0);
|
|
253
|
+
rb_define_module_function(m, "protect_inner", cast(AnyMethod) &protect_inner, 0);
|
|
254
|
+
rb_define_module_function(m, "deep_ping_pong", cast(AnyMethod) &deep_ping_pong, 1);
|
|
255
|
+
rb_define_module_function(m, "identical_exc", cast(AnyMethod) &identical_exc, 0);
|
|
256
|
+
rb_define_module_function(m, "reraise", cast(AnyMethod) &reraise, 1);
|
|
257
|
+
rb_define_module_function(m, "calls", cast(AnyMethod) &get_calls, 0);
|
|
258
|
+
rb_define_module_function(m, "ensures", cast(AnyMethod) &get_ensures, 0);
|
|
259
|
+
rb_define_module_function(m, "rescues", cast(AnyMethod) &get_rescues, 0);
|
|
260
|
+
rb_define_module_function(m, "protects", cast(AnyMethod) &get_protects, 0);
|
|
261
|
+
rb_define_module_function(m, "max_depth", cast(AnyMethod) &get_max_depth, 0);
|
|
262
|
+
rb_define_module_function(m, "ping_pong_failed", cast(AnyMethod) &get_ping_fail, 0);
|
|
263
|
+
rb_define_module_function(m, "exc_kept", cast(AnyMethod) &get_exc_kept, 0);
|
|
264
|
+
rb_define_module_function(m, "exc_reseen", cast(AnyMethod) &get_exc_reseen, 0);
|
|
265
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/// EXPERIMENT 5 — TypedData allocation/free storm under GC pressure.
|
|
2
|
+
///
|
|
3
|
+
/// Torture elements:
|
|
4
|
+
/// - Ruby threads hammer DExpStorm.make(n) -> n malloc'd TypedData wrappers,
|
|
5
|
+
/// dropped immediately (Ruby GC churns them), each dfree bumps an ATOMIC
|
|
6
|
+
/// D-side counter: allocation/free must balance exactly at the end.
|
|
7
|
+
/// - While the Ruby storm runs, a D-side native thread churns D-GC memory
|
|
8
|
+
/// (large struct arrays) and forces GC.collect()s — exercising the D
|
|
9
|
+
/// collector while Ruby's collector runs in other OS threads.
|
|
10
|
+
/// - Cross-thread free counting proves dfree callbacks are actually invoked
|
|
11
|
+
/// (freed == allocated) and that typed-data free is thread-safe w.r.t.
|
|
12
|
+
/// a concurrently-running D GC.
|
|
13
|
+
///
|
|
14
|
+
/// Layout:
|
|
15
|
+
/// DExpStorm.make(n) -> allocate n wrappers, return the last one
|
|
16
|
+
/// DExpStorm.alloc_count -> total wrappers allocated (monotonic)
|
|
17
|
+
/// DExpStorm.free_count -> total dfree callbacks run
|
|
18
|
+
/// DExpStorm.start_churn / stop_churn -> D background GC churn on/off
|
|
19
|
+
/// DExpStorm.churn_cycles -> how many churn cycles completed
|
|
20
|
+
/// DExpStorm.balance -> alloc_count - free_count (== live wrappers)
|
|
21
|
+
///
|
|
22
|
+
/// What can go wrong (the point of the torture):
|
|
23
|
+
/// - dfree never called (leak) -> free_count stalls below alloc_count
|
|
24
|
+
/// - dfree called twice -> free_count > alloc_count (corruption)
|
|
25
|
+
/// - race on the counters between Ruby threads + D thread -> torn reads
|
|
26
|
+
/// (counters must be shared, atomic ops via core.atomic)
|
|
27
|
+
|
|
28
|
+
module exp_gcstorm;
|
|
29
|
+
|
|
30
|
+
import rudy.bind.value;
|
|
31
|
+
import rudy.convert;
|
|
32
|
+
import rudy.typeddata;
|
|
33
|
+
|
|
34
|
+
import core.atomic : atomicOp, atomicLoad;
|
|
35
|
+
import core.thread : Thread;
|
|
36
|
+
import core.time : msecs;
|
|
37
|
+
import core.memory : GC;
|
|
38
|
+
import core.runtime : rt_init;
|
|
39
|
+
import core.stdc.stdlib : malloc, free;
|
|
40
|
+
import std.conv : to;
|
|
41
|
+
|
|
42
|
+
private:
|
|
43
|
+
|
|
44
|
+
// A struct with real payload so malloc'd memory is actually used
|
|
45
|
+
struct StormCell
|
|
46
|
+
{
|
|
47
|
+
double[16] payload;
|
|
48
|
+
ulong serial;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// dfree for this experiment: count the free, then release the malloc'd
|
|
52
|
+
// cell. The free runs on Ruby's GC thread — a DIFFERENT OS thread than
|
|
53
|
+
// make() — which is exactly the cross-thread race the storm tests.
|
|
54
|
+
extern (C) void stormcell_free(void* p)
|
|
55
|
+
{
|
|
56
|
+
atomicOp!"+="(free_count, 1);
|
|
57
|
+
free(p);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// The mixin wires our counting dfree into the descriptor directly (third
|
|
61
|
+
// template param) — no need for a hand-built second descriptor, which the
|
|
62
|
+
// first version of this experiment did (and which was silently unused:
|
|
63
|
+
// stormcell_wrap always wrapped with the ORIGINAL descriptor, so free_count
|
|
64
|
+
// stayed 0 — torture finding #6: the mixin's wrap function hard-codes
|
|
65
|
+
// `&name_type`, so overriding .dfree on a copied descriptor does nothing).
|
|
66
|
+
mixin RudyTypedData!(StormCell, "stormcell", &stormcell_free);
|
|
67
|
+
|
|
68
|
+
shared long alloc_count; // bumped by make() under GVL (no race with dfree)
|
|
69
|
+
shared long free_count; // bumped by dfree from Ruby GC threads
|
|
70
|
+
shared long churn_cycles; // bumped by the D background thread
|
|
71
|
+
|
|
72
|
+
// ---------------------------------------------------------- Ruby API
|
|
73
|
+
|
|
74
|
+
extern (C) VALUE make(VALUE self, VALUE n)
|
|
75
|
+
{
|
|
76
|
+
long count = d_type!long(n);
|
|
77
|
+
VALUE last = Qnil_;
|
|
78
|
+
foreach (i; 0 .. count)
|
|
79
|
+
{
|
|
80
|
+
auto cell = cast(StormCell*) malloc(StormCell.sizeof);
|
|
81
|
+
cell.serial = cast(ulong) atomicOp!"+="(alloc_count, 1);
|
|
82
|
+
cell.payload[] = 1.5; // touch the memory
|
|
83
|
+
last = stormcell_wrap(rb_cObject, cell);
|
|
84
|
+
// immediately drop `last` from Ruby's perspective: not stored
|
|
85
|
+
// anywhere, next iteration overwrites the reference.
|
|
86
|
+
}
|
|
87
|
+
return last;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
extern (C) VALUE get_alloc_count(VALUE self) { return rb_int2inum(atomicLoad(alloc_count)); }
|
|
91
|
+
extern (C) VALUE get_free_count(VALUE self) { return rb_int2inum(atomicLoad(free_count)); }
|
|
92
|
+
extern (C) VALUE get_balance(VALUE self)
|
|
93
|
+
{
|
|
94
|
+
return rb_int2inum(atomicLoad(alloc_count) - atomicLoad(free_count));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ---------------------------------------------------------- D churn thread
|
|
98
|
+
|
|
99
|
+
shared bool churning; // D keyword-safe flag read by the thread
|
|
100
|
+
__gshared Thread churn_thread;
|
|
101
|
+
|
|
102
|
+
// Plain D function (NOT extern (C)): core.thread.Thread's ctor wants
|
|
103
|
+
// `void function()` and an extern (C) function has a different D type even
|
|
104
|
+
// though the machine code is identical.
|
|
105
|
+
void churn_loop()
|
|
106
|
+
{
|
|
107
|
+
while (atomicLoad(churning))
|
|
108
|
+
{
|
|
109
|
+
// churn: allocate and drop D-GC memory, forcing collections
|
|
110
|
+
auto junk = new StormCell[10_000];
|
|
111
|
+
foreach (ref c; junk) c.serial = 42;
|
|
112
|
+
atomicOp!"+="(churn_cycles, 1);
|
|
113
|
+
// D GC runs while Ruby GC frees wrappers in other threads
|
|
114
|
+
if (atomicLoad(churning))
|
|
115
|
+
GC.collect();
|
|
116
|
+
Thread.getThis.sleep(10.msecs);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
extern (C) VALUE start_churn(VALUE self)
|
|
121
|
+
{
|
|
122
|
+
if (churn_thread !is null) return Qfalse_;
|
|
123
|
+
churning = true;
|
|
124
|
+
churn_thread = new Thread(&churn_loop);
|
|
125
|
+
churn_thread.isDaemon = true;
|
|
126
|
+
churn_thread.start();
|
|
127
|
+
return Qtrue_;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
extern (C) VALUE stop_churn(VALUE self)
|
|
131
|
+
{
|
|
132
|
+
churning = false;
|
|
133
|
+
if (churn_thread !is null)
|
|
134
|
+
{
|
|
135
|
+
churn_thread.join();
|
|
136
|
+
churn_thread = null;
|
|
137
|
+
}
|
|
138
|
+
return Qnil_;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
extern (C) VALUE get_churn_cycles(VALUE self) { return rb_int2inum(atomicLoad(churn_cycles)); }
|
|
142
|
+
|
|
143
|
+
// ---------------------------------------------------------- init
|
|
144
|
+
|
|
145
|
+
pragma(mangle, "Init_exp_gcstorm")
|
|
146
|
+
extern (C) void Init_exp_gcstorm()
|
|
147
|
+
{
|
|
148
|
+
// TORTURE FINDING #5: a GDC-compiled .so dlopened by Ruby NEVER runs
|
|
149
|
+
// rt_init(). The D GC lazy-initializes on first `new`, but core.time's
|
|
150
|
+
// _ticksPerSecond is filled only by _d_initMonoTime() from rt_init().
|
|
151
|
+
// Calling MonoTime/Thread.sleep from a D thread then hard-aborts with
|
|
152
|
+
// "MonoTimeImpl failed to get the frequency of the system's monotonic
|
|
153
|
+
// clock." (druntime core/time.d abort) — seen as a process-wide SIGABRT
|
|
154
|
+
// racing the Ruby VM. Any D extension that spawns threads, sleeps, or
|
|
155
|
+
// uses GC.collect() must call rt_init() itself in its Init hook.
|
|
156
|
+
rt_init();
|
|
157
|
+
|
|
158
|
+
// descriptor is generated by the mixin with the counting free func
|
|
159
|
+
// wired in via template parameter — nothing to patch here.
|
|
160
|
+
|
|
161
|
+
VALUE m = rb_define_module("DExpStorm");
|
|
162
|
+
rb_define_module_function(m, "make", cast(AnyMethod) &make, 1);
|
|
163
|
+
rb_define_module_function(m, "alloc_count", cast(AnyMethod) &get_alloc_count, 0);
|
|
164
|
+
rb_define_module_function(m, "free_count", cast(AnyMethod) &get_free_count, 0);
|
|
165
|
+
rb_define_module_function(m, "balance", cast(AnyMethod) &get_balance, 0);
|
|
166
|
+
rb_define_module_function(m, "start_churn", cast(AnyMethod) &start_churn, 0);
|
|
167
|
+
rb_define_module_function(m, "stop_churn", cast(AnyMethod) &stop_churn, 0);
|
|
168
|
+
rb_define_module_function(m, "churn_cycles", cast(AnyMethod) &get_churn_cycles, 0);
|
|
169
|
+
}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/// EXPERIMENT 4 — A tiny single-endpoint HTTP server: D owns the socket,
|
|
2
|
+
/// Ruby owns the handler.
|
|
3
|
+
///
|
|
4
|
+
/// Torture elements:
|
|
5
|
+
/// - POSIX socket code entirely in D (std.socket), accept() runs with the
|
|
6
|
+
/// GVL RELEASED via rb_thread_call_without_gvl + RUBY_UBF_IO — the same
|
|
7
|
+
/// pattern any real blocking C extension must use.
|
|
8
|
+
/// - The response body is produced by a Ruby block (proc) handed in from
|
|
9
|
+
/// Ruby and invoked via rb_funcallv — D <-> Ruby boundary crossing on a
|
|
10
|
+
/// hot path, with fresh Ruby objects (request string) each call.
|
|
11
|
+
/// - A Ruby side thread stays alive while D blocks in accept(): proves the
|
|
12
|
+
/// GVL is actually released (side thread's heartbeats would stall it).
|
|
13
|
+
/// - rb_ensure wraps the whole serve-one-request cycle so the client socket
|
|
14
|
+
/// is closed even if the handler block raises mid-response.
|
|
15
|
+
///
|
|
16
|
+
/// Layout (port chosen by kernel, reported back to Ruby via serve.port):
|
|
17
|
+
///
|
|
18
|
+
/// DExpHTTP.serve_one(port_block_latch) — binds 127.0.0.1:0, accepts ONE
|
|
19
|
+
/// request, calls the registered handler block with the request
|
|
20
|
+
/// string, writes "HTTP/1.1 200 OK\r\nContent-Length: N\r\n\r\n" +
|
|
21
|
+
/// body, closes, returns the port used.
|
|
22
|
+
/// DExpHTTP.port — the bound port after serve_one.
|
|
23
|
+
/// DExpHTTP.register_handler(&blk) — stores the handler proc.
|
|
24
|
+
/// DExpHTTP.heartbeat — tick counter bumped by a Ruby
|
|
25
|
+
/// side thread; serve_one returns it alongside the port so the test
|
|
26
|
+
/// DExpHTTP.handler_ok — true if the Ruby handler was
|
|
27
|
+
/// actually called and returned a String.
|
|
28
|
+
///
|
|
29
|
+
/// What can go wrong (the point of the torture):
|
|
30
|
+
/// - handler block raising with client fd open (ensure must close it)
|
|
31
|
+
/// - GC moving the request string while D holds a slice (we .idup)
|
|
32
|
+
/// - accept() blocking forever with no UBF (we pass RUBY_UBF_IO; without
|
|
33
|
+
/// releasing the GVL a Ruby watchdog thread could never run)
|
|
34
|
+
|
|
35
|
+
module exp_http;
|
|
36
|
+
|
|
37
|
+
import rudy.bind.value;
|
|
38
|
+
import rudy.convert;
|
|
39
|
+
import rudy.defs;
|
|
40
|
+
|
|
41
|
+
import std.socket;
|
|
42
|
+
import std.string : toStringz, fromStringz;
|
|
43
|
+
import std.conv : to;
|
|
44
|
+
import std.format : format;
|
|
45
|
+
|
|
46
|
+
private:
|
|
47
|
+
|
|
48
|
+
// ---- state kept alive across calls ------------------------------------
|
|
49
|
+
// TORTURE FINDING #1: D module-level variables are THREAD-LOCAL by default.
|
|
50
|
+
// Ruby threads are OS pthreads — a variable set by one Ruby thread is NOT
|
|
51
|
+
// visible to another unless declared `__gshared` (C-style global). D1-era
|
|
52
|
+
// Rudy never hit this because D1 had no TLS globals. Without __gshared
|
|
53
|
+
// the port/heartbeat handshakes below silently read 0 from other threads.
|
|
54
|
+
__gshared:
|
|
55
|
+
TcpSocket listening_sock;
|
|
56
|
+
Socket client_sock; // NOT TcpSocket: Socket.accept() returns a base
|
|
57
|
+
// Socket instance; cast(TcpSocket) is a checked
|
|
58
|
+
// downcast in D and would yield null (torture
|
|
59
|
+
// finding #3), giving a null-deref on receive().
|
|
60
|
+
ushort bound_port;
|
|
61
|
+
int heartbeats; // bumped via DExpHTTP.heartbeat from Ruby
|
|
62
|
+
int handler_called; // set when the Ruby handler block ran
|
|
63
|
+
int accept_failed; // set by do_accept on a D exception
|
|
64
|
+
VALUE handler_blk = Qnil_; // the Ruby handler proc
|
|
65
|
+
|
|
66
|
+
// ubf for interrupting a blocking accept when the Ruby thread is killed
|
|
67
|
+
extern (C) void ubf_accept(void* unused)
|
|
68
|
+
{
|
|
69
|
+
// RUBY_UBF_IO is what real extensions use for socket waits: it makes
|
|
70
|
+
// the GVL-released call interruptible on Thread#kill/timeout without
|
|
71
|
+
// us implementing a real unblocker (the fd gets closed by the OS on
|
|
72
|
+
// process exit; for a one-shot experiment this is acceptable).
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
extern (C) void* do_accept(void* data)
|
|
76
|
+
{
|
|
77
|
+
// running WITHOUT the GVL: pure D socket code, no Ruby API calls.
|
|
78
|
+
// accept() returns a Socket (D GC-allocated), not TcpSocket — cast.
|
|
79
|
+
//
|
|
80
|
+
// TORTURE FINDING #2: when the process gets a signal (timeout(1) sends
|
|
81
|
+
// SIGTERM), accept(2) fails with EINTR and Phobos throws
|
|
82
|
+
// SocketAcceptException — and a D exception unwinding out of a
|
|
83
|
+
// rb_thread_call_without_gvl callback aborts the runtime (gcc.deh
|
|
84
|
+
// terminate: no D handler up-stack). All D exceptions must be
|
|
85
|
+
// swallowed inside the no-GVL region and reported after re-acquiring.
|
|
86
|
+
try
|
|
87
|
+
{
|
|
88
|
+
client_sock = listening_sock.accept();
|
|
89
|
+
accept_failed = false;
|
|
90
|
+
}
|
|
91
|
+
catch (Exception e)
|
|
92
|
+
{
|
|
93
|
+
accept_failed = true;
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ---- the Ruby handler invocation (GVL held again here) ----------------
|
|
99
|
+
|
|
100
|
+
extern (C) VALUE call_handler_protected(VALUE data)
|
|
101
|
+
{
|
|
102
|
+
// data = the request String built by D
|
|
103
|
+
VALUE blk = handler_blk;
|
|
104
|
+
VALUE[1] av = [data];
|
|
105
|
+
handler_called = 1;
|
|
106
|
+
return rb_funcallv(blk, rb_intern("call"), 1, av.ptr);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
extern (C) VALUE ensure_close_client(VALUE data)
|
|
110
|
+
{
|
|
111
|
+
// runs on the way out no matter what: success, handler raise, or a
|
|
112
|
+
// transport error. Closing twice is fine (second is a no-op).
|
|
113
|
+
if (client_sock !is null)
|
|
114
|
+
{
|
|
115
|
+
client_sock.close();
|
|
116
|
+
client_sock = null;
|
|
117
|
+
}
|
|
118
|
+
return Qnil_;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
extern (C) VALUE serve_one_body(VALUE self)
|
|
122
|
+
{
|
|
123
|
+
// bind 127.0.0.1:0 -> kernel picks a free port
|
|
124
|
+
auto addr = new InternetAddress("127.0.0.1", 0);
|
|
125
|
+
listening_sock = new TcpSocket(AddressFamily.INET);
|
|
126
|
+
listening_sock.bind(addr);
|
|
127
|
+
listening_sock.listen(1);
|
|
128
|
+
bound_port = (cast(InternetAddress) listening_sock.localAddress()).port;
|
|
129
|
+
|
|
130
|
+
// accept() with the GVL released — Ruby threads stay alive while we wait
|
|
131
|
+
rb_thread_call_without_gvl(&do_accept, null, &ubf_accept, null);
|
|
132
|
+
if (accept_failed)
|
|
133
|
+
rudy_raise(rb_eRuntimeError, "accept() failed (interrupted or bad fd)");
|
|
134
|
+
|
|
135
|
+
// read the request (curl/ruby client sends one small request; we read
|
|
136
|
+
// whatever arrived, up to a sane cap). Plain default-init is fine here.
|
|
137
|
+
ubyte[4096] buf;
|
|
138
|
+
size_t got = client_sock.receive(buf[]);
|
|
139
|
+
if (got == 0) // client closed without sending
|
|
140
|
+
rudy_raise(rb_eRuntimeError, "client closed before sending");
|
|
141
|
+
|
|
142
|
+
string req = cast(string) buf[0 .. got].idup;
|
|
143
|
+
|
|
144
|
+
// hand the request to the Ruby handler block
|
|
145
|
+
VALUE reqv = rb_str_new(req.ptr, cast(C_long) req.length);
|
|
146
|
+
int state;
|
|
147
|
+
VALUE body = rb_protect(&call_handler_protected, reqv, &state);
|
|
148
|
+
if (state)
|
|
149
|
+
{
|
|
150
|
+
// re-raise as a plain RuntimeError carrying the original message:
|
|
151
|
+
// proves the longjmp out of a D frame stack didn't corrupt state.
|
|
152
|
+
VALUE e = rb_errinfo();
|
|
153
|
+
VALUE msg = rb_funcallv(e, rb_intern("message"), 0, null);
|
|
154
|
+
string m = "handler raised: " ~ d_type!string(msg);
|
|
155
|
+
rudy_raise(rb_eRuntimeError, m);
|
|
156
|
+
}
|
|
157
|
+
VALUE body_s = rb_str_to_str(body); // enforce String contract
|
|
158
|
+
auto len = rb_str_strlen(body_s);
|
|
159
|
+
auto bptr = rb_string_value_ptr(&body_s); // may swap body_s in-place
|
|
160
|
+
|
|
161
|
+
// build + write the response
|
|
162
|
+
string head = format!"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: %d\r\nConnection: close\r\n\r\n"(len);
|
|
163
|
+
client_sock.send(head);
|
|
164
|
+
client_sock.send(bptr[0 .. len]);
|
|
165
|
+
|
|
166
|
+
// return [port, request_first_line] to Ruby
|
|
167
|
+
VALUE res = rb_ary_new_capa(2);
|
|
168
|
+
rb_ary_push(res, rb_int2inum(bound_port));
|
|
169
|
+
size_t line1_len = req.indexOf('\n'); // index of first '\n', or full len
|
|
170
|
+
VALUE line1 = rb_str_new(req.ptr, cast(C_long) line1_len);
|
|
171
|
+
rb_ary_push(res, line1);
|
|
172
|
+
return res;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
extern (C) VALUE serve_one(VALUE self)
|
|
176
|
+
{
|
|
177
|
+
handler_called = 0;
|
|
178
|
+
bound_port = 0; // so Ruby's port>0 poll waits for THIS round's listener
|
|
179
|
+
// also drop any leftover listener from a previous round
|
|
180
|
+
if (listening_sock !is null)
|
|
181
|
+
{
|
|
182
|
+
try { listening_sock.close(); } catch (Exception e) {}
|
|
183
|
+
listening_sock = null;
|
|
184
|
+
}
|
|
185
|
+
// rb_ensure: body, body_data, ensure, ensure_data
|
|
186
|
+
return rb_ensure(&serve_one_body, Qnil_, &ensure_close_client, Qnil_);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
extern (C) VALUE reset(VALUE self)
|
|
190
|
+
{
|
|
191
|
+
// close any leftover listener (e.g. after a raise round) and zero the
|
|
192
|
+
// port so a new round's client poll waits for a fresh bind
|
|
193
|
+
if (listening_sock !is null)
|
|
194
|
+
{
|
|
195
|
+
try { listening_sock.close(); } catch (Exception e) {}
|
|
196
|
+
listening_sock = null;
|
|
197
|
+
}
|
|
198
|
+
if (client_sock !is null)
|
|
199
|
+
{
|
|
200
|
+
try { client_sock.close(); } catch (Exception e) {}
|
|
201
|
+
client_sock = null;
|
|
202
|
+
}
|
|
203
|
+
bound_port = 0;
|
|
204
|
+
return Qnil_;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
extern (C) VALUE get_port(VALUE self) { return rb_int2inum(bound_port); }
|
|
208
|
+
extern (C) VALUE get_heartbeats(VALUE self) { return rb_int2inum(heartbeats); }
|
|
209
|
+
extern (C) VALUE heartbeat(VALUE self) { heartbeats += 1; return Qnil_; }
|
|
210
|
+
extern (C) VALUE handler_ok(VALUE self) { return handler_called ? Qtrue_ : Qfalse_; }
|
|
211
|
+
|
|
212
|
+
extern (C) VALUE register_handler(VALUE self)
|
|
213
|
+
{
|
|
214
|
+
// Called as DExpHTTP.register_handler { |req| ... } — arity 0, the block
|
|
215
|
+
// is materialized via rb_block_proc (raises if no block was given).
|
|
216
|
+
handler_blk = rb_block_proc();
|
|
217
|
+
return handler_blk;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// so the side-thread test can bump the counter from Ruby
|
|
221
|
+
extern (C) VALUE exphttp_note(VALUE self, VALUE note)
|
|
222
|
+
{
|
|
223
|
+
VALUE m = rb_define_module("ExpHTTPNotes");
|
|
224
|
+
rb_iv_set(m, "@note", note);
|
|
225
|
+
return note;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// VOID_INIT helper removed — plain `ubyte[4096] buf;` works and avoids
|
|
229
|
+
// template-instantiation pitfalls under GDC.
|
|
230
|
+
|
|
231
|
+
// min/indexOf shims (avoid pulling whole std.algorithm for two names)
|
|
232
|
+
private size_t indexOf(string s, char c)
|
|
233
|
+
{
|
|
234
|
+
foreach (i, dchar ch; s)
|
|
235
|
+
if (ch == c) return i;
|
|
236
|
+
return s.length;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
pragma(mangle, "Init_exp_http")
|
|
240
|
+
extern (C) void Init_exp_http()
|
|
241
|
+
{
|
|
242
|
+
VALUE m = rb_define_module("DExpHTTP");
|
|
243
|
+
rb_define_module_function(m, "serve_one",
|
|
244
|
+
cast(AnyMethod) &serve_one, 0);
|
|
245
|
+
rb_define_module_function(m, "reset",
|
|
246
|
+
cast(AnyMethod) &reset, 0);
|
|
247
|
+
rb_define_module_function(m, "register_handler",
|
|
248
|
+
cast(AnyMethod) ®ister_handler, 0);
|
|
249
|
+
rb_define_module_function(m, "port", cast(AnyMethod) &get_port, 0);
|
|
250
|
+
rb_define_module_function(m, "heartbeats", cast(AnyMethod) &get_heartbeats, 0);
|
|
251
|
+
rb_define_module_function(m, "heartbeat", cast(AnyMethod) &heartbeat, 0);
|
|
252
|
+
rb_define_module_function(m, "handler_ok", cast(AnyMethod) &handler_ok, 0);
|
|
253
|
+
rb_gc_register_address(&handler_blk);
|
|
254
|
+
}
|