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,282 @@
1
+ /// Object-protocol boilerplate for RudyTypedData-wrapped structs.
2
+ ///
3
+ /// A bare RudyTypedData!() wrapper gives Ruby a pointer-holding T_DATA
4
+ /// object and nothing else:
5
+ /// - `dup`/`clone` shallow-copy the WRAPPER — both handles end up
6
+ /// pointing at the same struct (finding #19),
7
+ /// - `inspect` shows the useless default `#<Class:0x...>`,
8
+ /// - two wrappers holding byte-identical structs are never `==`,
9
+ /// - `Object#hash` is identity-based, so == wrappers don't hash equal
10
+ /// and can't be Hash keys,
11
+ /// - `freeze` sets the flag but nothing enforces it: a mutator writes
12
+ /// straight through into the "frozen" struct (finding #20).
13
+ ///
14
+ /// This mixin generates the missing pieces, C-API style:
15
+ ///
16
+ /// struct Vec2 { double x, y; }
17
+ /// mixin RudyTypedData!(Vec2, "vec2");
18
+ /// mixin RudyObjectProtocol!(Vec2, "vec2",
19
+ /// null, null, null,
20
+ /// vec2_inspect); // hooks optional
21
+ /// // in Init_*:
22
+ /// vec2_proto_install(klass);
23
+ ///
24
+ /// `name` must be the SAME name passed to RudyTypedData — the generated
25
+ /// code references `<name>_get`, `<name>_type` and the class name.
26
+ ///
27
+ /// What install() defines on the class:
28
+ /// allocate rb_data_typed_object_zalloc — a zeroed T in Ruby's
29
+ /// GC heap; dfree still runs at collection.
30
+ /// initialize_copy struct-assignment in place (PRIVATE, like Ruby's
31
+ /// own). Ruby implements dup AND clone through this
32
+ /// one hook: dup = alloc + initialize_copy, clone =
33
+ /// alloc + initialize_copy + frozen-state propagation.
34
+ /// Optional copyHook runs after the struct copy
35
+ /// (deep-copy malloc'd fields; see struct-copy rules).
36
+ /// == / eql? default: memcmp over T.sizeof. Non-wrapper other
37
+ /// -> Qfalse_ (== must not raise). eql? aliases ==.
38
+ /// hash default: FNV-1a over T.sizeof bytes — memcmp-equal
39
+ /// structs hash equal, keeping the ==/hash contract.
40
+ /// inspect "#<ClassName inner>" via inspectHook; with no hook
41
+ /// install() skips inspect entirely (Ruby's default
42
+ /// stays).
43
+ ///
44
+ /// Custom hooks (all optional, pass null to take the default). Hooks
45
+ /// are ALIAS parameters — pass the symbol BARE (no `&`): GDC 14 rejects
46
+ /// `&fn` as a template value argument entirely (finding #22). Pointer
47
+ /// and ref call conventions both work per hook:
48
+ /// copyHook (T* dst, const(T*) src) or (ref T, ref const(T))
49
+ /// equalsHook (const(T*) a, const(T*) b) or (ref const(T), ref const(T))
50
+ /// hashHook (const(T*) p) or (ref const(T))
51
+ /// inspectHook (const(T*) p) or (ref const(T))
52
+ ///
53
+ /// Hook users own the ==/hash contract: if equalsHook says two structs
54
+ /// are equal, hashHook must return the same value for both.
55
+ ///
56
+ /// Struct-copy rules for initialize_copy (NB):
57
+ /// - pointers embedded in T are copied by VALUE (shallow). Use copyHook
58
+ /// to deep-copy malloc'd fields — free the old buffer first or the
59
+ /// allocate() zeroing leaks it on the fresh wrapper.
60
+ /// - D-GC pointers inside T do NOT survive the copy by D-GC rules: the
61
+ /// struct bytes live in Ruby's GC heap where D's collector cannot
62
+ /// see them, so a new wrapper's struct may point at memory D's GC
63
+ /// considers unreachable. Root such pointers D-side (GC.addRange /
64
+ /// a module-scope anchor) — malloc-owned fields (freed in a custom
65
+ /// dfree) are the safe pattern.
66
+
67
+ module rudy.objproto;
68
+
69
+ import rudy.bind.value;
70
+ import rudy.convert;
71
+ import rudy.typeddata;
72
+
73
+ import core.stdc.string : memcmp;
74
+ import std.string : toStringz;
75
+
76
+ public:
77
+
78
+ // Symbols the generated string-mixins reference. String-mixin bodies
79
+ // resolve names at the mixin SITE (the user's module), not here — so
80
+ // every module that instantiates RudyObjectProtocol must be able to see
81
+ // them. `public import` re-exports from any module that imports
82
+ // rudy.objproto.
83
+ public import rudy.bind.value : VALUE, Qfalse_, rb_data_typed_object_zalloc,
84
+ rb_define_alloc_func, rb_define_private_method, rb_define_method,
85
+ rb_define_alias, rb_obj_class, rb_class2name, rb_str_new_cstr, rb_str_cat,
86
+ AnyMethod;
87
+ public import rudy.convert : to_ruby_value;
88
+ public import rudy.typeddata : RbDataType, RudyTypedData;
89
+
90
+ // ---------------------------------------------------------------- helpers
91
+
92
+ /// FNV-1a over n bytes — the default hash for memcmp-style ==.
93
+ size_t rudy_fnv1a(const void* p, size_t n) @nogc nothrow pure
94
+ {
95
+ size_t h = 0xcbf29ce484222325UL;
96
+ auto b = cast(const ubyte*) p;
97
+ foreach (i; 0 .. n)
98
+ {
99
+ h ^= b[i];
100
+ h *= 0x100000001b3UL;
101
+ }
102
+ return h;
103
+ }
104
+
105
+ // ---------------------------------------------------------------- mixin
106
+
107
+ // static-if for string-mixin bodies: pick one code snippet at CT.
108
+ // Lives inside the template below (per-instance) — multiple
109
+ // RudyObjectProtocol instantiations in one module cannot collide.
110
+ // Ruby's Hash#hash values are signed; mask off the sign bit so the
111
+ // unsigned machine word decodes as a non-negative Integer everywhere.
112
+ // Public so generated mixin code (resolved at the mixin site) can use it.
113
+ public enum size_t rudy_hash_mask = 0x7fff_ffff_ffff_ffffUL;
114
+
115
+ /// memcmp-style byte equality over n bytes — the default == for
116
+ /// memcmp-able structs. Public for the same reason as rudy_hash_mask.
117
+ public bool rudy_bytes_eq(const void* a, const void* b, size_t n)
118
+ {
119
+ return memcmp(a, b, n) == 0;
120
+ }
121
+
122
+ /// Hooks are ALIAS template parameters — GDC 14 rejects `&fn` as a
123
+ /// template value argument for typed fp params (finding #22: even
124
+ /// extern(C)-linkage; only default values like typeddata's
125
+ /// `= &rudy_typed_free` parse, because defaults are normal expressions,
126
+ /// not template arguments). Pass hook symbols BARE:
127
+ ///
128
+ /// mixin RudyObjectProtocol!(Vec2, "vec2", null, null, null, vec2_inspect);
129
+ ///
130
+ /// Hook presence is probed with __traits(compiles, hook(args...)) —
131
+ /// no `!is null` checks (an alias may not support that). Wrong-arity
132
+ /// or non-callable aliases safely take the default path. Both call
133
+ /// conventions are accepted per hook:
134
+ /// size_t hashHook(const(T*) p) — pointer style
135
+ /// size_t hashHook(ref const(T) p) — ref style
136
+ /// (the mixin probes ptr-call first, then deref-call, at compile time).
137
+ template RudyObjectProtocol(T,
138
+ string name,
139
+ alias copyHook = null,
140
+ alias equalsHook = null,
141
+ alias hashHook = null,
142
+ alias inspectHook = null)
143
+ {
144
+ // staticIf lives INSIDE the template: instantiated per-param, never
145
+ // collides across multiple instantiations in one module.
146
+ private template staticIf(bool cond, string yes, string no)
147
+ {
148
+ static if (cond)
149
+ enum string staticIf = yes;
150
+ else
151
+ enum string staticIf = no;
152
+ }
153
+
154
+ // Presence probes — compile-time, per hook. Try pointer-style call
155
+ // first, then ref-style (deref) call.
156
+ private enum string copyCall =
157
+ staticIf!(__traits(compiles, copyHook(cast(T*) null, cast(T*) null)),
158
+ " copyHook(d, s);",
159
+ staticIf!(__traits(compiles, copyHook(*cast(T*) null, *cast(T*) null)),
160
+ " copyHook(*d, *s);",
161
+ ""));
162
+
163
+ private enum string eqCall =
164
+ staticIf!(__traits(compiles, equalsHook(cast(const(T)*) null, cast(const(T)*) null)),
165
+ " return to_ruby_value(equalsHook(a, b));",
166
+ staticIf!(__traits(compiles, equalsHook(*cast(const(T)*) null, *cast(const(T)*) null)),
167
+ " return to_ruby_value(equalsHook(*a, *b));",
168
+ ""));
169
+
170
+ private enum string hashCall =
171
+ staticIf!(__traits(compiles, hashHook(cast(const(T)*) null)),
172
+ " return to_ruby_value(hashHook(p) & rudy_hash_mask);",
173
+ staticIf!(__traits(compiles, hashHook(*cast(const(T)*) null)),
174
+ " return to_ruby_value(hashHook(*p) & rudy_hash_mask);",
175
+ ""));
176
+
177
+ private enum string inspCall =
178
+ staticIf!(__traits(compiles, inspectHook(cast(const(T)*) null)),
179
+ " string inner = inspectHook(p);",
180
+ staticIf!(__traits(compiles, inspectHook(*cast(const(T)*) null)),
181
+ " string inner = inspectHook(*p);",
182
+ ""));
183
+
184
+ private enum bool hasEquals = eqCall.length > 0;
185
+ private enum bool hasHash = hashCall.length > 0;
186
+ private enum bool hasInspect = inspCall.length > 0;
187
+
188
+ // ---- generate the extern(C) methods via a string mixin ----------
189
+
190
+ private enum string eqBody =
191
+ " T* a = " ~ name ~ "_get(self);" ~
192
+ " if (!rb_typeddata_is_kind_of(other, &" ~ name ~ "_type))" ~
193
+ " return Qfalse_;" ~
194
+ " T* b = " ~ name ~ "_get(other);" ~
195
+ staticIf!(hasEquals,
196
+ eqCall,
197
+ " return to_ruby_value(rudy_bytes_eq(a, b, T.sizeof));");
198
+
199
+ private enum string hashBody =
200
+ " T* p = " ~ name ~ "_get(self);" ~
201
+ staticIf!(hasHash,
202
+ hashCall,
203
+ " return to_ruby_value(rudy_fnv1a(p, T.sizeof) & rudy_hash_mask);");
204
+
205
+ // inspect: generated ONLY with a hook (else Ruby's default stands).
206
+ private static if (hasInspect)
207
+ private enum string inspectFn =
208
+ "extern (C) VALUE " ~ name ~ "_proto_inspect(VALUE self)" ~
209
+ "{" ~
210
+ " T* p = " ~ name ~ "_get(self);" ~
211
+ inspCall ~
212
+ " VALUE out_ = rb_str_new_cstr(\"#<\");" ~
213
+ " VALUE klass = rb_obj_class(self);" ~
214
+ " const(char)* cn = rb_class2name(klass);" ~
215
+ " size_t cnlen = 0; while (cn[cnlen]) cnlen++;" ~
216
+ " rb_str_cat(out_, cn, cast(long) cnlen);" ~
217
+ " rb_str_cat(out_, \": \", 2);" ~
218
+ " rb_str_cat(out_, inner.ptr, cast(long) inner.length);" ~
219
+ " rb_str_cat(out_, \">\", 1);" ~
220
+ " return out_;" ~
221
+ "}";
222
+ else
223
+ private enum string inspectFn = "";
224
+
225
+ private static if (hasInspect)
226
+ private enum string inspectDef =
227
+ " rb_define_method(klass, \"inspect\", cast(AnyMethod) &" ~
228
+ name ~ "_proto_inspect, 0);";
229
+ else
230
+ private enum string inspectDef = "";
231
+
232
+ // ---- install(): defines everything on the class ------------------
233
+ //
234
+ // allocate: zalloc zeroes T INSIDE Ruby's GC heap (prototype
235
+ // confirmed against the 4.0.7 headers: (klass, size_t size, type)).
236
+ // The 2010-era binding dropped the size argument and silently worked
237
+ // through register reuse on x86-64 (finding #21) — T.sizeof is now
238
+ // passed correctly.
239
+
240
+ mixin(
241
+ "extern (C) VALUE " ~ name ~ "_proto_alloc(VALUE klass)" ~
242
+ "{" ~
243
+ " return cast(VALUE) rb_data_typed_object_zalloc(klass, T.sizeof, &" ~
244
+ name ~ "_type);" ~
245
+ "}" ~
246
+
247
+ "extern (C) VALUE " ~ name ~ "_proto_icopy(VALUE self, VALUE orig)" ~
248
+ "{" ~
249
+ " T* d = " ~ name ~ "_get(self);" ~
250
+ " T* s = " ~ name ~ "_get(orig);" ~
251
+ " *d = *s;" ~
252
+ copyCall ~
253
+ " return self;" ~
254
+ "}" ~
255
+
256
+ "extern (C) VALUE " ~ name ~ "_proto_eq(VALUE self, VALUE other)" ~
257
+ "{" ~
258
+ eqBody ~
259
+ "}" ~
260
+
261
+ "extern (C) VALUE " ~ name ~ "_proto_hash(VALUE self)" ~
262
+ "{" ~
263
+ hashBody ~
264
+ "}" ~
265
+
266
+ inspectFn ~
267
+
268
+ "public void " ~ name ~ "_proto_install(VALUE klass)" ~
269
+ "{" ~
270
+ " rb_define_alloc_func(klass, cast(VALUE function(VALUE)) &" ~
271
+ name ~ "_proto_alloc);" ~
272
+ " rb_define_private_method(klass, \"initialize_copy\"," ~
273
+ " cast(AnyMethod) &" ~ name ~ "_proto_icopy, 1);" ~
274
+ " rb_define_method(klass, \"==\", cast(AnyMethod) &" ~
275
+ name ~ "_proto_eq, 1);" ~
276
+ " rb_define_alias(klass, \"eql?\", \"==\");" ~
277
+ " rb_define_method(klass, \"hash\", cast(AnyMethod) &" ~
278
+ name ~ "_proto_hash, 0);" ~
279
+ inspectDef ~
280
+ "}"
281
+ );
282
+ }
@@ -0,0 +1,143 @@
1
+ /// Typed-data wrapping for D structs as Ruby objects.
2
+ ///
3
+ /// EXPERIMENT 1 (exp_wrap) discovered that `rb_data_object_get` is an inline
4
+ /// C macro (`RDATA(obj)->data`), not an exported symbol — it cannot be called
5
+ /// from D. The workaround used a D-side registry keyed by rb_obj_id.
6
+ ///
7
+ /// This module replaces that workaround with the modern TypedData API, all
8
+ /// of whose entry points ARE exported functions:
9
+ ///
10
+ /// - `rb_data_typed_object_wrap` — wrap with a `rb_data_type_t` descriptor
11
+ /// - `rb_check_typeddata` — unwrap with type checking
12
+ /// - `rb_data_typed_object_zalloc` — zero-allocated wrap (like alloc func)
13
+ ///
14
+ /// Usage:
15
+ ///
16
+ /// struct Matrix { ... }
17
+ /// // 1. define the data type descriptor (one per D struct type)
18
+ /// mixin RudyTypedData!(Matrix, "matrix"); // exposes: matrix_type
19
+ /// // matrix_wrap
20
+ /// // matrix_get
21
+ /// // 2. wrap
22
+ /// VALUE obj = matrix_wrap(m, klass);
23
+ /// // 3. unwrap (raises TypeError if not a Matrix wrapper)
24
+ /// Matrix* m = matrix_get(obj);
25
+ ///
26
+ /// dfree is generated automatically: `GC-free-able` structs (allocated by
27
+ /// D's GC) are freed by the GC; C-malloc'd structs are freed with free().
28
+ /// Pass your own free callback to the mixin if you need custom cleanup.
29
+
30
+ module rudy.typeddata;
31
+
32
+ import rudy.bind.value;
33
+ import rudy.convert;
34
+
35
+ public:
36
+
37
+ // ---------------------------------------------------------------- layout
38
+
39
+ /// D mirror of ruby.h's rb_data_type_struct (layout-compatible subset).
40
+ ///
41
+ /// Field name differences vs the C header (D keyword collisions / style):
42
+ /// C `function` sub-struct -> `function_` (`function` is a D keyword)
43
+ /// D-side structs only mirror the C layout; Ruby never reads the names.
44
+ struct RbDataFunction
45
+ {
46
+ RUBY_DATA_FUNC_MARK dmark;
47
+ RUBY_DATA_FUNC_FREE dfree;
48
+ extern (C) size_t function(const void*) dsize;
49
+ RUBY_DATA_FUNC_MARK dcompact;
50
+ void*[1] reserved; // must be zero
51
+ }
52
+
53
+ struct RbDataType
54
+ {
55
+ const(char)* wrap_struct_name;
56
+ RbDataFunction function_;
57
+ const(RbDataType)* parent;
58
+ void* data;
59
+ VALUE flags;
60
+ }
61
+
62
+ // re-export the callback aliases so users don't need value.d here
63
+ alias RUBY_DATA_FUNC_MARK = rudy.bind.value.RUBY_DATA_FUNC_MARK;
64
+ alias RUBY_DATA_FUNC_FREE = rudy.bind.value.RUBY_DATA_FUNC_FREE;
65
+
66
+ // ---------------------------------------------------------------- default free
67
+
68
+ /// Default dfree for structs allocated with D's GC: let D's GC handle it —
69
+ /// nothing to do at Ruby-GC time (pointer stays valid for D's collector).
70
+ extern (C) void rudy_typed_nofree(void* p) {}
71
+
72
+ /// Default dfree for structs allocated with malloc/calloc: free() them.
73
+ extern (C) void rudy_typed_free(void* p)
74
+ {
75
+ import core.stdc.stdlib : free;
76
+ free(p);
77
+ }
78
+
79
+ // ---------------------------------------------------------------- mixin template
80
+
81
+ /// Generates a typed-data descriptor + wrap/unwrap pair for struct T.
82
+ ///
83
+ /// mixin RudyTypedData!(Matrix, "matrix");
84
+ /// // provides: matrix_type (RbDataType),
85
+ /// // matrix_wrap(VALUE klass, Matrix*),
86
+ /// // matrix_get(VALUE obj)
87
+ ///
88
+ /// Pass a custom free callback as the third parameter for exotic ownership
89
+ /// models (default: malloc'd → rudy_typed_free; use &rudy_typed_nofree for
90
+ /// D-GC-owned structs).
91
+ ///
92
+ /// EXPERIMENT 7 (exp_compact) — finding #11: if the wrapped struct holds
93
+ /// live VALUEs and the descriptor has no dmark, the referents are
94
+ /// collectible while the wrapper is still alive: use-after-free (observed
95
+ /// as a garbage String read back from a freed-and-reused slot). If the
96
+ /// struct holds VALUEs, ALSO pass mark/compact callbacks:
97
+ ///
98
+ /// extern (C) void mycell_mark(void* p)
99
+ /// { rb_gc_mark_movable((cast(MyCell*) p).embedded); }
100
+ /// extern (C) void mycell_compact(void* p)
101
+ /// { auto c = cast(MyCell*) p; c.embedded = rb_gc_location(c.embedded); }
102
+ /// mixin RudyTypedData!(MyCell, "mycell", &rudy_typed_free,
103
+ /// &mycell_mark, &mycell_compact);
104
+ ///
105
+ /// dmark uses rb_gc_mark_movable and dcompact rewrites the slot via
106
+ /// rb_gc_location — the movable-objects protocol required by GC.compact.
107
+ ///
108
+ /// NB: the generated identifiers need string mixins — D template parameters
109
+ /// do NOT become identifier names in a plain mixin template body.
110
+ template RudyTypedData(T, string name,
111
+ RUBY_DATA_FUNC_FREE freeFunc = &rudy_typed_free,
112
+ RUBY_DATA_FUNC_MARK markFunc = null,
113
+ RUBY_DATA_FUNC_MARK compactFunc = null)
114
+ {
115
+ private enum cname = name ~ "\0"; // NUL-terminated, static storage
116
+
117
+ mixin(
118
+ "private extern (C) size_t " ~ name ~ "_dsize(const void* p)" ~
119
+ "{" ~
120
+ " return T.sizeof;" ~
121
+ "}" ~
122
+
123
+ // The descriptor. Must outlive every wrapped object.
124
+ "__gshared RbDataType " ~ name ~ "_type = RbDataType(" ~
125
+ " cname.ptr," ~
126
+ " RbDataFunction(markFunc, freeFunc, &" ~ name ~ "_dsize, compactFunc, [null])," ~
127
+ " null," ~ // parent
128
+ " null," ~ // data
129
+ " 0);" ~ // flags — zero is safest
130
+
131
+ /// Wrap a T* in a Ruby object of class `klass`.
132
+ "VALUE " ~ name ~ "_wrap(VALUE klass, T* ptr)" ~
133
+ "{" ~
134
+ " return rb_data_typed_object_wrap(klass, ptr, &" ~ name ~ "_type);" ~
135
+ "}" ~
136
+
137
+ /// Unwrap — raises TypeError on any non-matching wrapper.
138
+ "T* " ~ name ~ "_get(VALUE obj)" ~
139
+ "{" ~
140
+ " return cast(T*) rb_check_typeddata(obj, &" ~ name ~ "_type);" ~
141
+ "}"
142
+ );
143
+ }
@@ -0,0 +1,6 @@
1
+ # Loads the freshly built dextest.so with the *ruby 4.0.7* interpreter that
2
+ # also provided libruby at link time (extension .so must match ABI).
3
+ require "pathname"
4
+ EXT = Pathname.new(__dir__).parent + ENV.fetch("RUDY_BUILD_DIR", ".build") + "dextest.so"
5
+ raise "extension not built: run `make` first" unless EXT.exist?
6
+ require EXT.to_s
data/test/exitest.rb ADDED
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env ruby
2
+ # Acceptance suite for the missing-API bindings (dexitest).
3
+ # Exercises: rb_yield, rb_block_given_p, rb_yield_values2, rb_block_call,
4
+ # rb_rescue, rb_ensure, rb_define_singleton_method, rb_scan_args,
5
+ # rb_thread_call_without_gvl, variadic def! (D default args -> optional).
6
+
7
+ require "pathname"
8
+ EXT = Pathname.new(__dir__).parent + ".build" + "dexitest.so"
9
+ raise "extension not built: run `make` first" unless EXT.exist?
10
+ require EXT.to_s
11
+
12
+ $failures = 0
13
+ $count = 0
14
+
15
+ def assert(cond, msg)
16
+ $count += 1
17
+ if cond then puts "ok #{$count} - #{msg}"
18
+ else $failures += 1; puts "FAIL #{$count} - #{msg}" end
19
+ end
20
+
21
+ def assert_equal(expected, actual, msg = nil)
22
+ assert(expected == actual, "#{msg || 'equal'}: expected #{expected.inspect}, got #{actual.inspect}")
23
+ end
24
+
25
+ def assert_raises(exp)
26
+ yield
27
+ assert(false, "expected #{exp} to be raised")
28
+ rescue exp
29
+ assert(true, "raised #{exp}")
30
+ rescue => e
31
+ assert(false, "expected #{exp}, got #{e.class}: #{e.message}")
32
+ end
33
+
34
+ # ---------------------------------------------------------------- rb_yield
35
+ assert_equal(55, DExitTest.sum_to(10) { |i| i }, "rb_yield: sum 1..10 via block")
36
+ assert_equal(110, DExitTest.sum_to(10) { |i| i * 2 }, "rb_yield: block sees each value")
37
+
38
+ # ------------------------------------------- rb_block_given_p / yield_values2
39
+ assert_equal(-1, DExitTest.pair_sums(5), "rb_block_given_p: no block -> -1")
40
+ assert_equal(165, DExitTest.pair_sums(5) { |a, b| a + b },
41
+ "rb_yield_values2: |a, b| pairs (1,10)..(5,50) summed")
42
+ assert_equal(3850, DExitTest.pair_sums(10) { |a, b| a * b },
43
+ "rb_yield_values2: products summed")
44
+
45
+ # ---------------------------------------------------------------- rb_block_call
46
+ assert_equal([1, 2, 3], DExitTest.block_collect([1, 2, 3]),
47
+ "rb_block_call: C-implemented block collects each entry")
48
+ assert_equal(%w[a b c], DExitTest.block_collect(["a", "b", "c"]),
49
+ "rb_block_call: works on any enumerable responding to each")
50
+ assert_equal([], DExitTest.block_collect([]), "rb_block_call: empty array")
51
+
52
+ # ---------------------------------------------------------------- rb_rescue
53
+ assert_equal("rescued", DExitTest.div_rescue(1, 0), "rb_rescue: ZeroDivisionError -> handler")
54
+ assert_equal("rescued", DExitTest.div_rescue(1, 0) { |e| "not called" },
55
+ "rb_rescue: handler result returned, block irrelevant")
56
+ assert_equal(42, DExitTest.safe_rescue, "rb_rescue: success path returns b_proc value")
57
+
58
+ # ---------------------------------------------------------------- rb_ensure
59
+ assert_equal(true, DExitTest.ensure_success, "rb_ensure: ensure ran after success")
60
+ assert_raises(RuntimeError) { DExitTest.ensure_raises }
61
+ assert(DExitTest.ensure_raised_flag != 0 && DExitTest.ensure_raised_flag != false,
62
+ "rb_ensure: ensure ran despite raise (got #{DExitTest.ensure_raised_flag.inspect})")
63
+
64
+ # ------------------------------------------------- rb_define_singleton_method
65
+ obj = Object.new
66
+ DExitTest.make_singleton(obj)
67
+ assert_equal("hi from singleton", obj.dexitest_greet,
68
+ "rb_define_singleton_method: per-object method")
69
+ assert_raises(NoMethodError) { Object.new.dexitest_greet }
70
+
71
+ # ---------------------------------------------------------------- rb_scan_args
72
+ n, a, b, rest = DExitTest.scan(1)
73
+ assert_equal([1, 1, nil, []], [n, a, b, rest], 'rb_scan_args "11*": 1 mandatory')
74
+ n, a, b, rest = DExitTest.scan(1, 2)
75
+ assert_equal([2, 1, 2, []], [n, a, b, rest], 'rb_scan_args "11*": 1 mandatory + 1 optional')
76
+ n, a, b, rest = DExitTest.scan(1, 2, 3, 4)
77
+ assert_equal([4, 1, 2, [3, 4]], [n, a, b, rest], 'rb_scan_args "11*": splat rest')
78
+
79
+ # ---------------------------------------------------------------- GVL
80
+ r = DExitTest.nogvl
81
+ assert(r.is_a?(Integer) && r >= 0, "rb_thread_call_without_gvl: D nap returned")
82
+
83
+ # prove another Ruby thread ran while the GVL was released
84
+ main_result = Queue.new
85
+ t = Thread.new { main_result << DExitTest.nogvl_parallel }
86
+ sleep 0.01
87
+ DExitTest.tick # runs while the D nap holds no GVL
88
+ t.join
89
+ assert_equal(1, main_result.pop, "rb_thread_call_without_gvl: D nap + parallel Ruby thread OK")
90
+
91
+ # ---------------------------------------------------------------- variadic def!
92
+ assert_equal(33, DExitTest.var_sum(1), "variadic def!: a=1, defaults b=2 c=30")
93
+ assert_equal(33, DExitTest.var_sum(1, 2), "variadic def!: a=1 b=2, default c=30")
94
+ assert_equal(3, DExitTest.var_sum(1, 2, 0), "variadic def!: all three explicit")
95
+ assert_raises(ArgumentError) { DExitTest.var_sum }
96
+ assert_raises(ArgumentError) { DExitTest.var_sum(1, 2, 3, 4) }
97
+
98
+ assert_equal("hello, world!", DExitTest.var_greet("world"), "variadic def!: string defaults")
99
+ assert_equal("howdy, world?", DExitTest.var_greet("world", "howdy", "?"),
100
+ "variadic def!: all explicit")
101
+
102
+ assert_equal(10, DExitTest.var_raw(10), "variadic def!: raw VALUE passthrough, 1 arg")
103
+ assert_equal("abc", DExitTest.var_raw("abc"), "variadic def!: raw VALUE passthrough, string")
104
+ assert_equal(20, DExitTest.var_raw(10, 2), "variadic def!: raw VALUE + explicit optional arg")
105
+
106
+ puts
107
+ if $failures.zero?
108
+ puts "PASS: all #{$count} assertions green"
109
+ exit 0
110
+ else
111
+ puts "FAIL: #{$failures}/#{$count} assertions failed"
112
+ exit 1
113
+ end