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.
Files changed (57) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +114 -0
  3. data/LICENSE +21 -0
  4. data/Makefile +137 -0
  5. data/README.md +400 -0
  6. data/bin/rudy4 +6 -0
  7. data/examples/hello_gem/README.md +91 -0
  8. data/examples/hello_gem/ext/hello_gem/extconf.rb +75 -0
  9. data/examples/hello_gem/ext/hello_gem/hello_gem.d +122 -0
  10. data/examples/hello_gem/hello_gem.gemspec +35 -0
  11. data/examples/hello_gem/lib/hello_gem/version.rb +5 -0
  12. data/examples/hello_gem/lib/hello_gem.rb +32 -0
  13. data/examples/hello_gem/test/smoke.sh +81 -0
  14. data/experiments/exp_compact.d +178 -0
  15. data/experiments/exp_convert.d +93 -0
  16. data/experiments/exp_cross.d +144 -0
  17. data/experiments/exp_defargs.d +301 -0
  18. data/experiments/exp_exc.d +265 -0
  19. data/experiments/exp_gcstorm.d +169 -0
  20. data/experiments/exp_http.d +254 -0
  21. data/experiments/exp_nonregression.d +209 -0
  22. data/experiments/exp_objproto.d +123 -0
  23. data/experiments/exp_perf.d +81 -0
  24. data/experiments/exp_wrap.d +186 -0
  25. data/lib/rudy4/cli.rb +204 -0
  26. data/lib/rudy4/doctor.rb +295 -0
  27. data/lib/rudy4/error.rb +10 -0
  28. data/lib/rudy4/generator.rb +172 -0
  29. data/lib/rudy4/templates.rb +484 -0
  30. data/lib/rudy4/vendor.rb +36 -0
  31. data/lib/rudy4/version.rb +5 -0
  32. data/lib/rudy4.rb +16 -0
  33. data/scripts/abi_audit.sh +64 -0
  34. data/scripts/memcheck.sh +77 -0
  35. data/scripts/vendor_check.sh +33 -0
  36. data/scripts/vendor_sync.sh +25 -0
  37. data/src/dexitest.d +278 -0
  38. data/src/dextest.d +119 -0
  39. data/src/dtypetest.d +159 -0
  40. data/src/rudy/args.d +253 -0
  41. data/src/rudy/bind/value.d +306 -0
  42. data/src/rudy/convert.d +221 -0
  43. data/src/rudy/defs.d +440 -0
  44. data/src/rudy/objproto.d +282 -0
  45. data/src/rudy/typeddata.d +143 -0
  46. data/test/dextest_loader.rb +6 -0
  47. data/test/exitest.rb +113 -0
  48. data/test/rudy4_test.rb +291 -0
  49. data/test/run.rb +74 -0
  50. data/test/typedtest.rb +69 -0
  51. data/vendor/rudy/args.d +253 -0
  52. data/vendor/rudy/bind/value.d +306 -0
  53. data/vendor/rudy/convert.d +221 -0
  54. data/vendor/rudy/defs.d +440 -0
  55. data/vendor/rudy/objproto.d +282 -0
  56. data/vendor/rudy/typeddata.d +143 -0
  57. metadata +106 -0
@@ -0,0 +1,209 @@
1
+ /// EXPERIMENT 11 — Documented non-goals (backlog item 9).
2
+ ///
3
+ /// This experiment exists to put *evidence* behind the three documented
4
+ /// non-goals rather than silence:
5
+ ///
6
+ /// 1. Fiber switching across D frames — a Ruby Fiber may switch stacks
7
+ /// while D frames sit below on the fiber's stack. Simple value
8
+ /// round-trips work (D frames are plain C frames and survive the
9
+ /// switch), but D's GC registers the *thread* stack, not Ruby's
10
+ /// separate fiber stacks — so a D heap object reachable only from a
11
+ /// fiber-stack local has no root. NOT SUPPORTED as a guarantee; the
12
+ /// probes below show what does work and mark what is not.
13
+ /// 2. Ractor-shareability — a D-wrapped object is an opaque non-Ruby
14
+ /// object with a D-runtime-owned payload; it is not shareable and
15
+ /// cannot be moved to another Ractor safely. D runtime TLS also has
16
+ /// to initialise on the Ractor's foreign thread. NOT SUPPORTED.
17
+ /// 3. Interruptibility of a long GVL-held D call — while D holds the
18
+ /// GVL, Ruby cannot run its signal/interrupt checks, so Ctrl-C
19
+ /// during `fib(40)` is delivered only after the D call returns.
20
+ /// Inherent to holding the GVL, not a rudy bug (the GVL-released
21
+ /// variant IS interruptible — that is the whole point of
22
+ /// `rb_thread_call_without_gvl`).
23
+ ///
24
+ /// Probe surface (module `DNonReg`):
25
+ /// DNonReg.fib(n) — pure-D tree recursion (the "fib(40)" case)
26
+ /// DNonReg.sleep_gvl(ms) — busy-wait holding the GVL (uninterruptible)
27
+ /// DNonReg.nap_nogvl(ms) — busy-wait with the GVL released (interruptible)
28
+ /// DNonReg.helper_check() — new/delete a D object whose ctor takes a
29
+ /// live Ruby VALUE (per-thread D runtime probe)
30
+ /// DNonRegBox.new(x, y) — a D-wrapped (protocol'd) object to ferry
31
+ /// DNonRegBox#[] / #[]= — read / frozen-guarded write
32
+ /// DNonRegBox#roundtrip { } — read, Ruby-yield, read again (Fiber probe)
33
+ ///
34
+ /// The interruptibility probe lives in the Ruby suite (it forks a
35
+ /// subprocess and signals it); everything else is in-process.
36
+
37
+ module exp_nonregression;
38
+
39
+ import rudy.bind.value;
40
+ import rudy.convert;
41
+ import rudy.typeddata;
42
+ import rudy.objproto;
43
+ import rudy.defs;
44
+
45
+ import core.runtime : rt_init;
46
+ import core.time : MonoTime, msecs;
47
+ import std.string : format;
48
+
49
+ private:
50
+
51
+ // ------------------------------------------------------------------ fib
52
+ // The fresh-stack case from the backlog: a long, pure-D, GVL-held call.
53
+ long nr_fib(long n)
54
+ {
55
+ return n < 2 ? n : nr_fib(n - 1) + nr_fib(n - 2);
56
+ }
57
+
58
+ // ---------------------------------------------------- GVL / no-GVL waits
59
+ // Busy-wait holding the GVL: Ruby's signal checks cannot run until we
60
+ // return. This is the "Ctrl-C during fib(40) is late" case, made
61
+ // deterministic with a wall-clock bound instead of a CPU-bound fib.
62
+ long nr_sleep_gvl(long ms)
63
+ {
64
+ auto t0 = MonoTime.currTime;
65
+ while ((MonoTime.currTime - t0) < msecs(ms))
66
+ { /* spin with the GVL held */ }
67
+ return ms;
68
+ }
69
+
70
+ private __gshared long g_nogvl_ms = 0;
71
+ // Set by the unblock function when Ruby asks the blocking region to stop
72
+ // (e.g. SIGINT while the GVL is released). A COOPERATING body checks it;
73
+ // with a no-op ubf the wait would still run to completion.
74
+ private __gshared bool g_nogvl_abort = false;
75
+
76
+ extern (C) void* nr_nap_body(void* data)
77
+ {
78
+ auto t0 = MonoTime.currTime;
79
+ while (!g_nogvl_abort && (MonoTime.currTime - t0) < msecs(g_nogvl_ms))
80
+ { /* spin with the GVL released — no Ruby API touched */ }
81
+ return null;
82
+ }
83
+
84
+ extern (C) void nr_nap_ubf(void* data)
85
+ {
86
+ g_nogvl_abort = true; // the cancellation hook
87
+ }
88
+
89
+ // Same wait, but with the GVL released via rb_thread_call_without_gvl:
90
+ // Ruby keeps running on other threads and CAN deliver an interrupt — the
91
+ // ubf flips g_nogvl_abort and the body bails out early.
92
+ long nr_nap_nogvl(long ms)
93
+ {
94
+ g_nogvl_ms = ms;
95
+ g_nogvl_abort = false;
96
+ rb_thread_call_without_gvl(&nr_nap_body, null, &nr_nap_ubf, null);
97
+ return ms;
98
+ }
99
+
100
+ // ---------------------------------------------------- D runtime probe
101
+ // A D object whose constructor takes a live Ruby VALUE (so the
102
+ // constructor touches the D GC/runtime, not just the stack). Created and
103
+ // deleted entirely inside the call: from a Ractor this exercises
104
+ // druntime initialisation on a foreign thread.
105
+ class DHelper
106
+ {
107
+ VALUE tag_ = Qnil_;
108
+ long n_ = 0;
109
+
110
+ this(VALUE tag, long n)
111
+ {
112
+ tag_ = tag;
113
+ n_ = n;
114
+ }
115
+
116
+ long compute() const
117
+ {
118
+ return n_ * 2;
119
+ }
120
+
121
+ ~this() {}
122
+ }
123
+
124
+ long nr_helper_check()
125
+ {
126
+ auto h = new DHelper(rb_str_new("probe", 5), 21);
127
+ scope (exit) destroy(h);
128
+ return h.compute();
129
+ }
130
+
131
+ // ---------------------------------------------------- wrapped box
132
+ struct Box
133
+ {
134
+ double x, y;
135
+ }
136
+
137
+ mixin RudyTypedData!(Box, "box");
138
+
139
+ private string box_inspect(ref const Box p)
140
+ {
141
+ return format("x=%g y=%g", p.x, p.y);
142
+ }
143
+
144
+ mixin RudyObjectProtocol!(Box, "box", null, null, null, box_inspect);
145
+
146
+ // `box_proto_install` (generated) installs the allocator + initialize_copy
147
+ // + ==/eql?/hash/inspect. `box_get` (generated by RudyTypedData) reads the
148
+ // wrapped struct back out.
149
+
150
+ extern (C) VALUE box_init(VALUE self, VALUE x, VALUE y)
151
+ {
152
+ Box* p = box_get(self);
153
+ p.x = d_type!double(x);
154
+ p.y = d_type!double(y);
155
+ return self;
156
+ }
157
+
158
+ extern (C) VALUE box_nth(VALUE self, VALUE k)
159
+ {
160
+ Box* p = box_get(self);
161
+ return d_type!long(k) == 0 ? to_ruby_value(p.x) : to_ruby_value(p.y);
162
+ }
163
+
164
+ extern (C) VALUE box_set(VALUE self, VALUE k, VALUE v)
165
+ {
166
+ rb_check_frozen(self);
167
+ Box* p = box_get(self);
168
+ if (d_type!long(k) == 0) p.x = d_type!double(v);
169
+ else p.y = d_type!double(v);
170
+ return v;
171
+ }
172
+
173
+ // read -> yield to Ruby -> read again: the D frame and the wrapped struct
174
+ // must both survive a Fiber switch performed inside the block. The block
175
+ // is captured via the C API (rb_block_given_p + rb_block_proc) because
176
+ // this is a hand-written method; a def!'d function would take RubyBlock.
177
+ extern (C) VALUE box_roundtrip(VALUE self, VALUE)
178
+ {
179
+ Box* p = box_get(self);
180
+ double before = p.x;
181
+ if (rb_block_given_p() != 0)
182
+ {
183
+ VALUE proc = rb_block_proc();
184
+ rb_funcallv(proc, rb_intern("call"), 0, null);
185
+ }
186
+ double after = p.x;
187
+ return to_ruby_value(before == after ? before : -1.0);
188
+ }
189
+
190
+ public:
191
+
192
+ pragma(mangle, "Init_exp_nonregression")
193
+ extern (C) void Init_exp_nonregression()
194
+ {
195
+ rt_init();
196
+
197
+ VALUE m = rb_define_module("DNonReg");
198
+ def!("DNonReg", nr_fib, "fib");
199
+ def!("DNonReg", nr_sleep_gvl, "sleep_gvl");
200
+ def!("DNonReg", nr_nap_nogvl, "nap_nogvl");
201
+ def!("DNonReg", nr_helper_check, "helper_check");
202
+
203
+ VALUE c = rb_define_class("DNonRegBox", rb_cObject);
204
+ box_proto_install(c);
205
+ rb_define_method(c, "initialize", cast(AnyMethod) &box_init, 2);
206
+ rb_define_method(c, "[]", cast(AnyMethod) &box_nth, 1);
207
+ rb_define_method(c, "[]=", cast(AnyMethod) &box_set, 2);
208
+ rb_define_method(c, "roundtrip", cast(AnyMethod) &box_roundtrip, 0);
209
+ }
@@ -0,0 +1,123 @@
1
+ /// EXPERIMENT 10 — Object protocol for wrapped structs.
2
+ ///
3
+ /// A RudyTypedData wrapper without object-protocol methods is a
4
+ /// second-class Ruby citizen:
5
+ /// - dup/clone shallow-copy the WRAPPER: two handles, one struct —
6
+ /// mutating the copy mutates the "original" (finding #19)
7
+ /// - == never fires (Object#== is identity), Hash keys collide on
8
+ /// identity hash (finding #20),
9
+ /// - freeze() sets a flag nothing enforces: mutators write straight
10
+ /// through into the "frozen" struct (finding #20),
11
+ /// - inspect shows #<Class:0x...> (finding #21),
12
+ /// - allocate()/initialize_copy don't exist — Ruby-level dup/clone
13
+ /// crash with "allocator undefined" (finding #22).
14
+ ///
15
+ /// The RudyObjectProtocol mixin generates the C-API-standard fix:
16
+ /// allocate = rb_data_typed_object_zalloc (zeroed T in Ruby's heap)
17
+ /// initialize_copy = in-place struct copy (dup AND clone route
18
+ /// through this one hook; clone propagates frozen state)
19
+ /// == / eql? = memcmp over T.sizeof (custom equalsHook allowed)
20
+ /// hash = FNV-1a over T.sizeof (==/hash contract holds)
21
+ /// inspect = "#<Name inner>" via hook
22
+ /// And mutators guard themselves with rb_check_frozen — the exported
23
+ /// C-API freeze enforcement, wired at def! registration sites.
24
+ ///
25
+ /// Torture surface (Vec2, malloc'd in Init-set variants):
26
+ /// DVec2.new/[] — construct via protocol'd class
27
+ /// dup/clone — independence, frozen-state propagation
28
+ /// ==/hash — Hash keying, Dictionary-style lookups
29
+ /// inspect — human-readable output
30
+ /// freeze — FrozenError from guarded mutators, read methods OK
31
+ /// GC storm — dup'd wrappers + Ruby GC.collect + D GC.collect
32
+
33
+ module exp_objproto;
34
+
35
+ import rudy.bind.value;
36
+ import rudy.convert;
37
+ import rudy.typeddata;
38
+ import rudy.objproto;
39
+ import rudy.defs;
40
+
41
+ import core.runtime : rt_init;
42
+ import core.stdc.stdlib : malloc, free;
43
+ import core.memory : GC;
44
+ import std.string : format;
45
+
46
+ private:
47
+
48
+ struct Vec2
49
+ {
50
+ double x, y;
51
+ double extra; // padding makes byte-compare non-trivial
52
+ }
53
+
54
+ mixin RudyTypedData!(Vec2, "vec2");
55
+
56
+ private string vec2_inspect(ref const Vec2 p)
57
+ {
58
+ return format("x=%g y=%g", p.x, p.y);
59
+ }
60
+
61
+ mixin RudyObjectProtocol!(Vec2, "vec2",
62
+ null, null, null,
63
+ vec2_inspect);
64
+
65
+ __gshared VALUE vec2_klass;
66
+
67
+ // ---- mutators: rb_check_frozen wired by hand (the C-API guard) ----
68
+
69
+ extern (C) VALUE vec2_set(VALUE self, VALUE k, VALUE v)
70
+ {
71
+ rb_check_frozen(self);
72
+ Vec2* p = vec2_get(self);
73
+ switch (d_type!long(k)) {
74
+ case 0: p.x = d_type!double(v); break;
75
+ case 1: p.y = d_type!double(v); break;
76
+ default: p.extra = d_type!double(v); break;
77
+ }
78
+ return v;
79
+ }
80
+
81
+ extern (C) VALUE vec2_nth(VALUE self, VALUE k)
82
+ {
83
+ Vec2* p = vec2_get(self);
84
+ final switch (d_type!long(k)) {
85
+ case 0: return to_ruby_value(p.x);
86
+ case 1: return to_ruby_value(p.y);
87
+ case 2: return to_ruby_value(p.extra);
88
+ }
89
+ }
90
+
91
+ // ---- init ---------------------------------------------------------
92
+
93
+ pragma(mangle, "Init_exp_objproto")
94
+ extern (C) void Init_exp_objproto()
95
+ {
96
+ rt_init();
97
+
98
+ VALUE c = rb_define_class("DRudyVec", rb_cObject);
99
+ vec2_klass = c;
100
+
101
+ // the object protocol (allocate/initialize_copy/==/eql?/hash/inspect)
102
+ vec2_proto_install(c);
103
+
104
+ // simple 2-arg initialize (zalloc'd struct, then set)
105
+ rb_define_method(c, "initialize", cast(AnyMethod) &vec2_init, 2);
106
+ rb_define_method(c, "[]", cast(AnyMethod) &vec2_nth, 1);
107
+ rb_define_method(c, "[]=", cast(AnyMethod) &vec2_set, 2);
108
+ rb_define_method(c, "norm2", cast(AnyMethod) &vec2_norm2, 0);
109
+ }
110
+
111
+ extern (C) VALUE vec2_init(VALUE self, VALUE x, VALUE y)
112
+ {
113
+ Vec2* p = vec2_get(self);
114
+ p.x = d_type!double(x);
115
+ p.y = d_type!double(y);
116
+ return self;
117
+ }
118
+
119
+ extern (C) VALUE vec2_norm2(VALUE self)
120
+ {
121
+ Vec2* p = vec2_get(self);
122
+ return to_ruby_value(p.x * p.x + p.y * p.y);
123
+ }
@@ -0,0 +1,81 @@
1
+ /// EXPERIMENT 3 — Performance & GC torture.
2
+ ///
3
+ /// Torture elements:
4
+ /// - tight loops crossing the boundary with def!-converted args
5
+ /// - Ruby-side GC running while D holds wrapped pointers
6
+ /// - fibonacci recursion through the boundary each level (worst case)
7
+ /// - big arrays/hashes converted wholesale
8
+
9
+ module exp_perf;
10
+
11
+ import rudy.bind.value;
12
+ import rudy.convert;
13
+ import rudy.defs;
14
+ import std.string : toStringz;
15
+
16
+ private:
17
+
18
+ // classic def! — ulong params decode correctly now (issue #1 fixed):
19
+ // the trampoline used to pass TAGGED VALUEs to any ulong param.
20
+ ulong perf_fib(ulong n)
21
+ {
22
+ ulong fib(ulong k) { return k < 2 ? k : fib(k - 1) + fib(k - 2); }
23
+ // recursion stays in D — this measures D-side speed
24
+ return fib(n);
25
+ }
26
+
27
+ // boundary-crossing fib: each level goes back out to Ruby and returns
28
+ extern (C) VALUE perf_fib_boundary(VALUE self, VALUE n)
29
+ {
30
+ ulong k = d_type!ulong(n);
31
+ if (k < 2) return to_ruby_value(k);
32
+ VALUE[1] av1 = [to_ruby_value(k - 1)];
33
+ VALUE[1] av2 = [to_ruby_value(k - 2)];
34
+ VALUE mod = rb_const_get(rb_cObject, rb_intern("ExpPerf"));
35
+ ulong a = d_type!ulong(rb_funcallv(mod, rb_intern("fib_boundary"), 1, av1.ptr));
36
+ ulong b = d_type!ulong(rb_funcallv(mod, rb_intern("fib_boundary"), 1, av2.ptr));
37
+ return to_ruby_value(a + b);
38
+ }
39
+
40
+ // build a 100k-element Array of Floats in D, sum in D
41
+ extern (C) VALUE perf_build(VALUE self, VALUE n)
42
+ {
43
+ long len = d_type!long(n);
44
+ VALUE ary = rb_ary_new_capa(len);
45
+ double total = 0.0; // uninit double is NaN in D
46
+ foreach (i; 0 .. len)
47
+ {
48
+ double v = i * 0.5;
49
+ total += v;
50
+ rb_ary_push(ary, to_ruby_value(v));
51
+ }
52
+ // return [array, sum]
53
+ VALUE res = rb_ary_new_capa(2);
54
+ rb_ary_push(res, ary);
55
+ rb_ary_push(res, to_ruby_value(total));
56
+ return res;
57
+ }
58
+
59
+ // stress to_ruby_value on AAs (Hash) — 10k keys
60
+ extern (C) VALUE perf_hash(VALUE self, VALUE n)
61
+ {
62
+ long len = d_type!long(n);
63
+ int[int] aa;
64
+ foreach (int i; 0 .. cast(int) len)
65
+ aa[i] = i * i;
66
+ return to_ruby_value(aa);
67
+ }
68
+
69
+ public:
70
+
71
+ pragma(mangle, "Init_exp_perf")
72
+ extern (C) void Init_exp_perf()
73
+ {
74
+ VALUE m = rb_define_module("ExpPerf");
75
+ def!("ExpPerf", perf_fib, "fib");
76
+ rb_define_module_function(m, "fib_boundary", cast(AnyMethod) &perf_fib_boundary, 1);
77
+ rb_define_module_function(m, "build", cast(AnyMethod) &perf_build, 1);
78
+ rb_define_module_function(m, "hash", cast(AnyMethod) &perf_hash, 1);
79
+ rb_define_const(m, "PI", to_ruby_value(3.141592653589793));
80
+ rb_define_const(m, "GREETING", to_ruby_value("hello from D"));
81
+ }
@@ -0,0 +1,186 @@
1
+ /// EXPERIMENT 1 — Wrap a real D struct as a Ruby object.
2
+ ///
3
+ /// Torture elements:
4
+ /// - heap-allocated D state (Matrix) owned by a Ruby object via
5
+ /// rb_data_object_wrap with mark/free callbacks
6
+ /// - recursive algorithm (determinant by Laplace) crossing the D/Ruby
7
+ /// boundary on every level
8
+ /// - mutation from Ruby (set) vs read (get)
9
+ /// - GC pressure: allocate/discard many wrappers in a loop
10
+ ///
11
+ /// Uses VALUE passthrough args (trampoline-supported) for self.
12
+
13
+ module exp_wrap;
14
+
15
+ import rudy.bind.value;
16
+ import rudy.convert;
17
+ import rudy.defs;
18
+ import core.memory : GC;
19
+
20
+
21
+ private static class RudyWrapError : Error { this(string m) { super(m); } }
22
+
23
+ private:
24
+
25
+ struct Matrix
26
+ {
27
+ size_t rows, cols;
28
+ double* data; // malloc'd, freed by the wrap free func
29
+
30
+ ref double at(size_t r, size_t c) { return data[r * cols + c]; }
31
+ }
32
+
33
+ // FINDING: rb_data_object_get is an inline C macro (RDATA(obj)->data),
34
+ // NOT exported by libruby. Since rudy avoids struct access, we keep a
35
+ // D-side registry: wrapper VALUE (via rb_obj_id) -> Matrix*, cleaned up
36
+ // in the free callback via a reverse map.
37
+ private Matrix*[ulong] registry; // key: rb_obj_id(wrapper)
38
+ private ulong[void*] reverse_registry; // key: Matrix* -> rb_obj_id
39
+ private VALUE dmatrix_class; // set in Init, used by stress
40
+ private VALUE expwrap_keep; // Ruby Array that pins the latest wrapper
41
+
42
+ extern (C) void matrix_free(void* p)
43
+ {
44
+ import core.stdc.stdlib : free;
45
+ auto m = cast(Matrix*) p;
46
+ if (auto pid = p in reverse_registry)
47
+ {
48
+ registry.remove(*pid);
49
+ reverse_registry.remove(p);
50
+ }
51
+ if (m && m.data) free(m.data);
52
+ if (m) free(m);
53
+ }
54
+
55
+ private Matrix* get_matrix(VALUE self)
56
+ {
57
+ auto pid = rb_obj_id(self);
58
+ auto pm = pid in registry;
59
+ if (pm is null)
60
+ rudy_raise(rb_eRuntimeError, "unregistered DMatrix (internal error)");
61
+ return *pm;
62
+ }
63
+
64
+ // Matrix allocated by `alloc` — the RUBY_DATA_FUNC shape for alloc funcs
65
+ extern (C) VALUE dmat_alloc(VALUE klass)
66
+ {
67
+ import core.stdc.stdlib : calloc;
68
+ auto m = cast(Matrix*) calloc(1, Matrix.sizeof);
69
+ VALUE obj = rb_data_object_wrap(klass, cast(void*) m, null, &matrix_free);
70
+ // store registry entries keyed by object identity
71
+ auto id = rb_obj_id(obj);
72
+ registry[id] = m;
73
+ reverse_registry[m] = id;
74
+ return obj;
75
+ }
76
+
77
+ extern (C) VALUE dmat_initialize(VALUE self, VALUE r, VALUE c)
78
+ {
79
+ auto m = get_matrix(self);
80
+ m.rows = cast(size_t) d_type!long(r);
81
+ m.cols = cast(size_t) d_type!long(c);
82
+ import core.stdc.stdlib : calloc;
83
+ m.data = cast(double*) calloc(Matrix.sizeof, m.rows * m.cols);
84
+ return self;
85
+ }
86
+
87
+ extern (C) VALUE dmat_set(VALUE self, VALUE r, VALUE c, VALUE v)
88
+ {
89
+ auto m = get_matrix(self);
90
+ m.at(d_type!long(r), d_type!long(c)) = d_type!double(v);
91
+ return v;
92
+ }
93
+
94
+ extern (C) VALUE dmat_get(VALUE self, VALUE r, VALUE c)
95
+ {
96
+ auto m = get_matrix(self);
97
+ return to_ruby_value(m.at(d_type!long(r), d_type!long(c)));
98
+ }
99
+
100
+ double matrix_det(Matrix* m)
101
+ {
102
+ if (m.rows == 1) return m.at(0, 0);
103
+ if (m.rows == 2) return m.at(0, 0) * m.at(1, 1) - m.at(0, 1) * m.at(1, 0);
104
+ double det = 0.0; // NB: bare `double det;` is NaN in D!
105
+ foreach (j; 0 .. m.cols)
106
+ {
107
+ Matrix minor;
108
+ minor.rows = m.rows - 1;
109
+ minor.cols = m.cols - 1;
110
+ import core.stdc.stdlib : malloc;
111
+ minor.data = cast(double*) malloc(double.sizeof * minor.rows * minor.cols);
112
+ scope (exit) { import core.stdc.stdlib : free; free(minor.data); }
113
+ foreach (ri; 0 .. minor.rows)
114
+ foreach (ci; 0 .. minor.cols)
115
+ minor.at(ri, ci) =
116
+ m.at(ri + 1, ci < j ? ci : ci + 1);
117
+ double sign = (j % 2) ? -1.0 : 1.0;
118
+ det += sign * m.at(0, j) * matrix_det(&minor);
119
+ }
120
+ return det;
121
+ }
122
+
123
+ extern (C) VALUE dmat_det(VALUE self)
124
+ {
125
+ auto m = get_matrix(self);
126
+ return to_ruby_value(matrix_det(m));
127
+ }
128
+
129
+ extern (C) VALUE dmat_to_a(VALUE self)
130
+ {
131
+ auto m = get_matrix(self);
132
+ VALUE outer = rb_ary_new_capa(cast(C_long) m.rows);
133
+ foreach (r; 0 .. m.rows)
134
+ {
135
+ VALUE row = rb_ary_new_capa(cast(C_long) m.cols);
136
+ foreach (c; 0 .. m.cols)
137
+ rb_ary_push(row, to_ruby_value(m.at(r, c)));
138
+ rb_ary_push(outer, row);
139
+ }
140
+ return outer;
141
+ }
142
+
143
+ // stress: allocate & drop 100_000 wrappers; if free callbacks never fire,
144
+ // we leak ~100k * (Matrix + data) — watch RSS
145
+ extern (C) VALUE dmat_stress(VALUE self, VALUE n)
146
+ {
147
+ foreach (i; 0 .. d_type!long(n))
148
+ {
149
+ import core.stdc.stdlib : calloc, malloc;
150
+ auto m = cast(Matrix*) calloc(1, Matrix.sizeof);
151
+ m.rows = m.cols = 4;
152
+ m.data = cast(double*) malloc(double.sizeof * 16);
153
+ VALUE obj = rb_data_object_wrap(dmatrix_class, cast(void*) m, null, &matrix_free);
154
+ auto id = rb_obj_id(obj);
155
+ registry[id] = m;
156
+ reverse_registry[m] = id;
157
+ rb_ary_store(expwrap_keep, 0, obj); // keep only last; free func cleans registry
158
+ }
159
+ GC.collect;
160
+ // ask Ruby to GC too
161
+ VALUE mod = rb_const_get(rb_cObject, rb_intern("GC"));
162
+ rb_funcallv(mod, rb_intern("start"), 0, null);
163
+ return Qnil_;
164
+ }
165
+
166
+ public:
167
+
168
+ pragma(mangle, "Init_exp_wrap")
169
+ extern (C) void Init_exp_wrap()
170
+ {
171
+ VALUE cDMatrix = rb_define_class("DMatrix", rb_cObject);
172
+ rb_define_alloc_func(cDMatrix, &dmat_alloc);
173
+ rb_define_method(cDMatrix, "initialize", cast(AnyMethod) &dmat_initialize, 2);
174
+ rb_define_method(cDMatrix, "[]", cast(AnyMethod) &dmat_get, 2);
175
+ rb_define_method(cDMatrix, "[]=", cast(AnyMethod) &dmat_set, 3);
176
+ rb_define_method(cDMatrix, "det", cast(AnyMethod) &dmat_det, 0);
177
+ rb_define_method(cDMatrix, "to_a", cast(AnyMethod) &dmat_to_a, 0);
178
+ rb_define_method(cDMatrix, "stress", cast(AnyMethod) &dmat_stress, 1);
179
+ VALUE dext = rb_define_module("ExpWrap");
180
+ dmatrix_class = cDMatrix;
181
+ expwrap_keep = rb_ary_new();
182
+ rb_iv_set(dext, "@keep", expwrap_keep);
183
+ // root the keep-array so GC never collects it (it's reachable from the
184
+ // module, but belt & braces since we hold a raw D reference)
185
+ rb_gc_register_address(&expwrap_keep);
186
+ }