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,306 @@
1
+ /// Hand-written binding to the Ruby C API (Ruby 3.x / 4.x).
2
+ ///
3
+ /// Design notes:
4
+ /// - `VALUE` is `uintptr_t` on modern 64-bit CRuby. We never touch RBasic
5
+ /// or any R* struct layout: all access goes through exported functions
6
+ /// (rb_float_value, rb_ary_entry, rb_string_value_ptr, ...), the way the
7
+ /// Ruby project intends binary extensions to work post-1.9.
8
+ /// - Global variables (rb_cObject, rb_eArgError, ...) are BSS data symbols;
9
+ /// bound with `extern(C) extern __gshared`.
10
+ /// - `C_long` aliases mirror the old RuDy/PyD trick so the same templates
11
+ /// work on 32/64-bit.
12
+
13
+ module rudy.bind.value;
14
+
15
+ extern (C):
16
+
17
+ // ---------------------------------------------------------------- core types
18
+
19
+ alias ulong VALUE; /// Ruby object reference (uintptr_t, tagged)
20
+ alias long SIGNED_VALUE; /// intptr_t
21
+
22
+ // special constants (values are tag bits, identical on all modern rubies)
23
+ enum VALUE Qnil_ = cast(VALUE)0x04; // RUBY_Qnil
24
+ enum VALUE Qtrue_ = cast(VALUE)0x14; // RUBY_Qtrue
25
+ enum VALUE Qfalse_ = cast(VALUE)0x00; // RUBY_Qfalse
26
+ enum VALUE Qundef_ = cast(VALUE)0x24; // RUBY_Qundef (internal)
27
+
28
+ // Fixed-arity method callback shapes. For a fixed arity N, Ruby calls the
29
+ // C function as `VALUE f(VALUE self, VALUE a1, .., VALUE aN)`; the
30
+ // variadic (argc/argv) style is only used when the registered arity is < 0.
31
+ // rudy's def! always registers fixed arity, so trampolines below use these.
32
+ alias VALUE function(VALUE self) AnyMethod_0;
33
+ alias VALUE function(VALUE self, VALUE a1) AnyMethod_1;
34
+ alias VALUE function(VALUE self, VALUE a1, VALUE a2) AnyMethod_2;
35
+ alias VALUE function(VALUE self, VALUE a1, VALUE a2, VALUE a3) AnyMethod_3;
36
+ alias void* AnyMethod; // what rb_define_method actually takes
37
+
38
+ /// Callback shape for rb_protect: `VALUE (*protected)(VALUE data)`
39
+ alias VALUE function(VALUE) ProtectFn;
40
+
41
+ // ---------------------------------------------------------------- API fns
42
+
43
+ // ---- definitions ---------------------------------------------------------
44
+
45
+ VALUE rb_define_class(const(char)* name, VALUE superclass);
46
+ VALUE rb_define_module(const(char)* name);
47
+ VALUE rb_define_class_under(VALUE outer, const(char)* name, VALUE superclass);
48
+ VALUE rb_define_module_under(VALUE outer, const(char)* name);
49
+ void rb_extend_object(VALUE mod);
50
+ void rb_define_method(VALUE klass, const(char)* name, AnyMethod func, int argc);
51
+ void rb_define_module_function(VALUE mod, const(char)* name, AnyMethod func, int argc);
52
+ void rb_define_global_function(const(char)* name, AnyMethod func, int argc);
53
+ void rb_define_alloc_func(VALUE klass, VALUE function(VALUE) alloc);
54
+ void rb_define_alias(VALUE klass, const(char)* alt, const(char)* old);
55
+ void rb_include_module(VALUE klass, VALUE mod);
56
+ void rb_define_attr(VALUE klass, const(char)* name, int read, int write);
57
+ void rb_define_const(VALUE klass, const(char)* name, VALUE value);
58
+ void rb_define_class_variable(VALUE klass, const(char)* name, VALUE value);
59
+
60
+ ID rb_intern(const(char)* name);
61
+ alias long ID;
62
+
63
+ // ---- calling / conversions ------------------------------------------------
64
+
65
+ VALUE rb_funcall(VALUE recv, ID mid, int argc, ...);
66
+ VALUE rb_funcallv(VALUE recv, ID mid, int argc, const(VALUE)* argv);
67
+ VALUE rb_funcallv_public(VALUE recv, ID mid, int argc, const(VALUE)* argv);
68
+ // call mid with argv and a Proc/block (use for methods taking a block:
69
+ // sort, each, map...) — NOT passable as a positional arg via rb_funcallv
70
+ VALUE rb_funcall_with_block(VALUE recv, ID mid, int argc, const(VALUE)* argv, VALUE proc);
71
+ VALUE rb_obj_call_init(VALUE obj, int argc, const(VALUE)* argv);
72
+ VALUE rb_eval_string(const(char)* str);
73
+ VALUE rb_obj_alloc(VALUE klass);
74
+
75
+ // numbers
76
+ long rb_num2long(VALUE);
77
+ long rb_num2ll(VALUE);
78
+ double rb_num2dbl(VALUE);
79
+ VALUE rb_ull2inum(C_ulonglong);
80
+ VALUE rb_uint2inum(C_ulong);
81
+ ulong rb_num2ulong(VALUE);
82
+ VALUE rb_ll2inum(long);
83
+ int rb_num2int(VALUE); // raises RangeError outside int range
84
+ uint rb_num2uint(VALUE); // raises RangeError outside uint range
85
+ VALUE rb_int2inum(C_long); // C ABI: intptr_t — was misbound as int (torture
86
+ // finding #4: wrong width silently truncates on
87
+ // SysV x86-64 because of register reuse)
88
+ VALUE rb_float_new(double);
89
+ double rb_float_value(VALUE);
90
+ VALUE rb_obj_is_instance_of(VALUE, VALUE); // exported; rb_type is macro-only
91
+ VALUE rb_Integer(VALUE);
92
+
93
+ // strings
94
+ VALUE rb_str_new(const(char)* ptr, long len);
95
+ VALUE rb_str_new_cstr(const(char)* ptr);
96
+ VALUE rb_str_dup(VALUE);
97
+ void rb_str_modify(VALUE);
98
+ VALUE rb_str_cat(VALUE str, const(char)* ptr, long len);
99
+ VALUE rb_str_buf_cat(VALUE str, const(char)* ptr, long len);
100
+ VALUE rb_string_value(VALUE* value);
101
+ /// Correct signature: takes a pointer to the slot (may replace its
102
+ /// contents, hence volatile). See rstring.h:289.
103
+ char* rb_string_value_ptr(VALUE* ptr);
104
+ const(char)* rb_string_value_cstr(VALUE str);
105
+ long rb_str_strlen(VALUE);
106
+ VALUE rb_str_to_str(VALUE str);
107
+ VALUE rb_obj_as_string(VALUE obj);
108
+ VALUE rb_any_to_s(VALUE obj);
109
+
110
+ // arrays
111
+ VALUE rb_ary_new();
112
+ VALUE rb_ary_new_capa(C_long capa);
113
+ VALUE rb_ary_new_from_values(C_long n, const(VALUE)* vals);
114
+ VALUE rb_ary_push(VALUE ary, VALUE item);
115
+ VALUE rb_ary_entry(VALUE ary, long offset);
116
+ VALUE rb_ary_store(VALUE ary, long idx, VALUE item);
117
+ VALUE rb_ary_join(VALUE ary, VALUE sep);
118
+ VALUE rb_ary_to_s(VALUE ary);
119
+ VALUE rb_obj_class(VALUE obj);
120
+ const(char)* rb_class2name(VALUE klass); // exported (class.c); name of class/module
121
+
122
+ // hashes
123
+ VALUE rb_hash_new();
124
+ VALUE rb_hash_lookup2(VALUE hash, VALUE key, VALUE def); // no Hash#default raise
125
+ VALUE rb_hash_aset(VALUE hash, VALUE key, VALUE val);
126
+ VALUE rb_hash_aref(VALUE hash, VALUE key);
127
+ size_t rb_hash_size(VALUE hash);
128
+
129
+ // ivars / constants
130
+ VALUE rb_iv_get(VALUE obj, const(char)* name);
131
+ VALUE rb_iv_set(VALUE obj, const(char)* name, VALUE value);
132
+ VALUE rb_const_get(VALUE klass, ID name);
133
+ void rb_const_set(VALUE klass, ID name, VALUE value);
134
+
135
+ // equality / inspection
136
+ VALUE rb_equal(VALUE a, VALUE b);
137
+ VALUE rb_obj_id(VALUE obj);
138
+ VALUE rb_inspect(VALUE obj);
139
+ VALUE rb_id2sym(ID);
140
+ VALUE rb_sym2str(VALUE sym);
141
+ const(char)* rb_id2name(ID);
142
+ VALUE rb_obj_frozen_p(VALUE obj);
143
+ void rb_check_frozen(VALUE obj); // raises FrozenError when frozen
144
+ VALUE rb_obj_freeze(VALUE obj); // returns self, sets FL_FREEZE
145
+ int rb_typeddata_is_kind_of(VALUE obj, in void* data_type); // non-raising
146
+ // (check_typeddata raises; this doesn't)
147
+
148
+ // exceptions / control flow
149
+ // (rb_raise/rb_fatal never return; D has no `noreturn` attribute for extern(C))
150
+ void rb_raise(VALUE exc, const(char)* fmt, ...);
151
+ void rb_fatal(const(char)* fmt, ...);
152
+ void rb_throw(const(char)* tag, VALUE val);
153
+ void rb_throw_obj(VALUE tag, VALUE val);
154
+ void rb_exit(int status);
155
+ VALUE rb_protect(ProtectFn fn, VALUE data, int* state);
156
+ // Re-raise an existing exception object (the only way to re-raise the SAME
157
+ // exception VALUE from D — rb_raise formats a NEW exception). noreturn.
158
+ void rb_exc_raise(VALUE exc);
159
+ // After a rb_protect state != 0 (or rb_rescue/rb_ensure rescue path), the
160
+ // pending exception is fetched via rb_errinfo. Without binding it D cannot
161
+ // inspect what Ruby raised while crossing the boundary.
162
+ VALUE rb_errinfo();
163
+ void rb_set_errinfo(VALUE err);
164
+ // int rb_check_arity(int argc, int min, int max); -- inline-only in ruby/internal/intern/error.h, NOT exported; keep out of bindings
165
+
166
+ // GC
167
+ void rb_gc_register_address(VALUE* address);
168
+ void rb_gc_unregister_address(VALUE* address);
169
+ VALUE rb_gc_location(VALUE); // remap a VALUE after GC.compact moved it
170
+ void rb_gc_mark(VALUE); // conservative mark (no compaction update)
171
+ void rb_gc_mark_movable(VALUE); // mark + this slot is updated via rb_gc_location in dcompact
172
+ VALUE rb_gc_enable();
173
+ VALUE rb_gc_disable();
174
+
175
+ // introspection
176
+ int rb_check_funcall(VALUE recv, ID mid, int argc, const(VALUE)* argv);
177
+ int rb_respond_to(VALUE obj, ID mid);
178
+
179
+ // blocks / iteration (bound for PR#4 — issue #4)
180
+ // rb_block_call_func signature (ruby/internal/iterator.h):
181
+ // VALUE f(VALUE yielded_arg, VALUE callback_arg, int argc,
182
+ // const VALUE* argv, VALUE blockarg)
183
+ alias VALUE function(VALUE yielded_arg, VALUE callback_arg, int argc,
184
+ const(VALUE)* argv, VALUE blockarg) BlockCallFunc;
185
+ alias VALUE function(VALUE data) BodyFunc; // rb_rescue/rb_ensure b_proc
186
+ alias VALUE function(VALUE data2, VALUE exc) RescueFunc; // rb_rescue r_proc
187
+ VALUE rb_yield(VALUE val);
188
+ VALUE rb_yield_values(int n, ...);
189
+ VALUE rb_yield_values2(int n, const(VALUE)* argv);
190
+ VALUE rb_yield_splat(VALUE ary);
191
+ VALUE rb_yield_block(VALUE yielded_arg, VALUE callback_arg, int argc,
192
+ const(VALUE)* argv, VALUE blockarg); // as rb_block_call_func
193
+ VALUE rb_block_call(VALUE obj, ID mid, int argc, const(VALUE)* argv,
194
+ BlockCallFunc proc, VALUE data2);
195
+ int rb_block_given_p();
196
+ VALUE rb_block_proc(); // materialize the passed block as a Proc VALUE
197
+ VALUE rb_each(VALUE obj);
198
+
199
+ // structured exception handling from D (only rb_protect was bound before)
200
+ VALUE rb_rescue(BodyFunc b_proc, VALUE data1, RescueFunc r_proc, VALUE data2);
201
+ // rb_rescue2: variadic list of exception classes terminated by (VALUE)0.
202
+ // D has no C varargs forwarding — bind rb_vrescue2 (takes va_list) is not
203
+ // portable either; the common single-class case can use rb_rescue. For
204
+ // multiple classes call with the variadic ABI is NOT provided; use
205
+ // rb_protect + rb_respond_to rescue semantics at Ruby level instead.
206
+ // (Kept out deliberately: calling variadic C from D is not possible.)
207
+ VALUE rb_ensure(BodyFunc b_proc, VALUE data1, BodyFunc e_proc, VALUE data2);
208
+
209
+ // per-object methods
210
+ void rb_define_singleton_method(VALUE obj, const(char)* mid, AnyMethod func, int arity);
211
+ void rb_define_private_method(VALUE klass, const(char)* mid, AnyMethod func, int arity);
212
+ void rb_define_protected_method(VALUE klass, const(char)* mid, AnyMethod func, int arity);
213
+
214
+ // GVL (Ruby threading interop)
215
+ alias void function(void*) UnblockFunc; // rb_unblock_function_t
216
+ alias void* function(void*) NoGvlFunc; // func passed to *_without_gvl
217
+ void* rb_thread_call_without_gvl(NoGvlFunc func, void* data1,
218
+ UnblockFunc ubf, void* data2);
219
+ void* rb_thread_call_without_gvl2(NoGvlFunc func, void* data1,
220
+ UnblockFunc ubf, void* data2);
221
+ void* rb_thread_call_with_gvl(NoGvlFunc func, void* data1);
222
+ enum void* RUBY_UBF_IO = cast(void*) -1; // special UBF for blocking IO
223
+
224
+ // argument scanning (the C variadic stores into VALUE* slots)
225
+ int rb_scan_args(int argc, const(VALUE)* argv, const(char)* fmt, ...);
226
+
227
+ // struct wrapping (minimal; not used by core rudy)
228
+ alias void function(void* ptr) RUBY_DATA_FUNC_MARK;
229
+ alias void function(void* ptr) RUBY_DATA_FUNC_FREE;
230
+ VALUE rb_data_object_wrap(VALUE klass, void* ptr,
231
+ RUBY_DATA_FUNC_MARK mark, RUBY_DATA_FUNC_FREE free);
232
+ // void* rb_data_object_get(VALUE obj); -- inline-only in ruby/internal/core/rdata.h, NOT exported (use typeddata helper)
233
+
234
+ // typed-data (rb_data_object_get is an inline macro; these are real
235
+ // exported functions — see rudy/typeddata.d for a D-friendly helper)
236
+ // NOTE: RbDataType (rb_data_type_t) is defined in rudy/typeddata.d to keep
237
+ // this raw binding free of struct definitions; cast accordingly.
238
+ alias size_t function(const void*) RUBY_DATA_FUNC_DSIZE;
239
+ VALUE rb_data_typed_object_wrap(VALUE klass, void* ptr, in void* data_type);
240
+ void* rb_check_typeddata(VALUE obj, in void* data_type);
241
+ // NB: the C prototype is (VALUE klass, size_t size, const rb_data_type_t*)
242
+ // — the pre-audit binding dropped `size` and silently worked through
243
+ // register reuse (finding #21). Signature now matches the header.
244
+ void* rb_data_typed_object_zalloc(VALUE klass, size_t size, in void* data_type);
245
+
246
+ // ---------------------------------------------------------------- globals
247
+
248
+ extern __gshared VALUE rb_cObject;
249
+ extern __gshared VALUE rb_cInteger;
250
+ extern __gshared VALUE rb_cFloat;
251
+ extern __gshared VALUE rb_cString;
252
+ extern __gshared VALUE rb_cArray;
253
+ extern __gshared VALUE rb_cHash;
254
+ extern __gshared VALUE rb_mKernel;
255
+
256
+ extern __gshared VALUE rb_eException;
257
+ extern __gshared VALUE rb_eStandardError;
258
+ extern __gshared VALUE rb_eRuntimeError;
259
+ extern __gshared VALUE rb_eTypeError;
260
+ extern __gshared VALUE rb_eArgError;
261
+ extern __gshared VALUE rb_eRangeError;
262
+ extern __gshared VALUE rb_eNameError;
263
+ extern __gshared VALUE rb_eNoMethodError;
264
+ extern __gshared VALUE rb_eNotImpError;
265
+ extern __gshared VALUE rb_eStopIteration;
266
+ extern __gshared VALUE rb_eKeyError;
267
+ extern __gshared VALUE rb_eIndexError;
268
+ extern __gshared VALUE rb_eLoadError;
269
+ extern __gshared VALUE rb_eSystemExit;
270
+ extern __gshared VALUE rb_eSysStackError; // Ruby 4 exports rb_eSysStackError (rb_eSystemStackError never existed)
271
+ extern __gshared VALUE rb_eZeroDivError;
272
+ extern __gshared VALUE rb_eLocalJumpError; // yield with no block / blk.call when none given
273
+ extern __gshared VALUE rb_eFrozenError; // rb_check_frozen on a frozen object
274
+
275
+ // type tags (ruby_value_type) — subset rudy needs
276
+ enum int RUBY_T_NONE = 0x00;
277
+ enum int RUBY_T_OBJECT = 0x01;
278
+ enum int RUBY_T_CLASS = 0x02;
279
+ enum int RUBY_T_MODULE = 0x03;
280
+ enum int RUBY_T_FLOAT = 0x0b;
281
+ enum int RUBY_T_STRING = 0x0d;
282
+ enum int RUBY_T_SYMBOL = 0x0f;
283
+ enum int RUBY_T_ARRAY = 0x11;
284
+ enum int RUBY_T_HASH = 0x13;
285
+ enum int RUBY_T_TRUE = 0x20;
286
+ enum int RUBY_T_FALSE = 0x21;
287
+ enum int RUBY_T_NIL = 0x22;
288
+ enum int RUBY_T_DATA = 0x26;
289
+ enum int RUBY_T_COMPLEX = 0x2a;
290
+
291
+ // C `long` on this platform (LP64 = 64 bits). Keeps to_ruby_value/d_type
292
+ // templates portable to 32-bit builds.
293
+ version (X86_64) {
294
+ alias long C_long;
295
+ alias ulong C_ulong;
296
+ } else version (X86) {
297
+ alias int C_long;
298
+ alias uint C_ulong;
299
+ } else {
300
+ alias long C_long;
301
+ alias ulong C_ulong;
302
+ }
303
+ alias long C_longlong;
304
+ alias ulong C_ulonglong;
305
+
306
+
@@ -0,0 +1,221 @@
1
+ /// Two-way conversions between native D values and Ruby VALUEs.
2
+ /// D2 rewrite of the original RuDy make_object.d (2010), now calling only
3
+ /// exported Ruby functions — no struct peeking.
4
+
5
+ module rudy.convert;
6
+
7
+ import rudy.bind.value;
8
+ import std.traits;
9
+ import std.string : toStringz;
10
+ import std.exception : enforce;
11
+
12
+
13
+ // ---------------------------------------------------------------- helpers
14
+
15
+ /// Opt-in raw-VALUE passthrough for def!'d function parameters.
16
+ ///
17
+ /// VALUE identity(VALUE v) ... -> DECODED as ulong (VALUE is aliased
18
+ /// to ulong, and d_type now decodes it instead of passing the raw
19
+ /// tagged pointer through).
20
+ ///
21
+ /// Raw!VALUE identity(Raw!VALUE v) ... -> raw, untouched VALUE.
22
+ ///
23
+ /// Rationale: VALUE is `alias ulong VALUE`, so "parameter is VALUE" can't
24
+ /// be detected as a distinct type — and a plain `ulong` param used to hit
25
+ /// the old raw-passthrough branch, receiving the TAGGED value (Ruby 5
26
+ /// arrived as 11). Passthrough is now explicit: wrap in Raw!(...) to say
27
+ /// "give me the bits untouched".
28
+ struct Raw(T)
29
+ {
30
+ T value;
31
+ }
32
+
33
+ /// Compile-time check: is T the Raw!(...) passthrough wrapper?
34
+ template isRaw(T)
35
+ {
36
+ static if (is(T == Raw!R, R))
37
+ enum isRaw = true;
38
+ else
39
+ enum isRaw = false;
40
+ }
41
+
42
+ /// Type traits the old rudy/ replaced its meta/ directory for.
43
+ template isDynamicArrayType(T) { enum bool isDynamicArrayType = isDynamicArray!T; }
44
+ template isStaticArrayType(T) { enum bool isStaticArrayType = isStaticArray!T; }
45
+ template isAssocArrayType(T) { enum bool isAssocArrayType = is(T == V[K], V, K); }
46
+
47
+ /// Byte length of a Ruby String, the exported-API way. History: the old
48
+ /// code sliced rb_str_strlen (a CHARACTER count) as a byte count —
49
+ /// truncating every multibyte string (finding #12) — and the natural fix
50
+ /// target rb_str_bytes_len turned out to be inline-macro-only, NOT
51
+ /// exported (caught locally by a symbol-lookup crash; the ABI audit would
52
+ /// have caught it in CI). Ruby's own String#bytesize is the exact answer;
53
+ /// rb_intern lazily, idempotently (VM is alive inside any method call).
54
+ private __gshared ID rudy_id_bytesize;
55
+
56
+ private long rudy_bytesize(VALUE str)
57
+ {
58
+ if (!rudy_id_bytesize)
59
+ rudy_id_bytesize = rb_intern("bytesize");
60
+ return cast(long) rb_num2ll(rb_funcallv(str, rudy_id_bytesize, 0, null));
61
+ }
62
+
63
+ /// Reject negative values where the D target type is unsigned: the
64
+ /// numeric family (rb_num2ulong / rb_num2uint) would otherwise wrap
65
+ /// them 2's-complement style (-1 arrived as 2^64-1, finding #13 family).
66
+ private void rudy_check_unsigned(VALUE val)
67
+ {
68
+ // Sign via double: rb_num2long would itself raise on values above
69
+ // LONG_MAX (2^64-1 is a legitimate ulong). Sign survives the cast;
70
+ // NaN/inf fail the comparison and get caught by num2ulong below.
71
+ if (rb_num2dbl(val) < 0)
72
+ rb_raise(rb_eRangeError, "negative value not convertible to unsigned D type");
73
+ }
74
+
75
+ /// Require a Ruby feature (e.g. "complex") before using it from D.
76
+ private void rb_require_c(string name)
77
+ {
78
+ VALUE req = rb_str_new(name.ptr, cast(C_long) name.length);
79
+ VALUE[1] av = [req];
80
+ rb_funcallv(rb_cObject, rb_intern("require"), 1, av.ptr);
81
+ }
82
+
83
+ // ---------------------------------------------------------------- to_ruby_value
84
+
85
+ /// Convert a native D value into a Ruby VALUE.
86
+ VALUE to_ruby_value(T)(T t)
87
+ {
88
+ static if (isRaw!T) // Raw!VALUE -> pass bits through
89
+ return t.value;
90
+ else static if (is(T == bool))
91
+ return t ? Qtrue_ : Qfalse_;
92
+
93
+ else static if (is(T == char))
94
+ return rb_int2inum(t);
95
+
96
+ else static if (is(T == ulong) || is(T == C_ulonglong))
97
+ return rb_ull2inum(cast(C_ulonglong) t); // unsigned encode path —
98
+ // the old signed
99
+ // rb_ll2inum(cast(long))
100
+ // turned 2^64-1 into -1
101
+ // (finding #15)
102
+
103
+ else static if (is(T : C_longlong))
104
+ return rb_ll2inum(cast(long) t);
105
+
106
+ else static if (is(T : C_long))
107
+ return rb_int2inum(cast(int) t);
108
+
109
+ else static if (is(T : double)) // float, real promote
110
+ return rb_float_new(cast(double) t);
111
+
112
+ else static if (is(T == creal) || is(T == cdouble))
113
+ {
114
+ rb_require_c("complex");
115
+ VALUE c = rb_const_get(rb_cObject, rb_intern("Complex"));
116
+ VALUE[2] av = [to_ruby_value(t.re), to_ruby_value(t.im)];
117
+ return rb_funcallv(c, rb_intern("new"), 2, av.ptr);
118
+ }
119
+
120
+ else static if (is(T == string))
121
+ return rb_str_new(t.ptr, cast(C_long) t.length);
122
+
123
+ else static if (isDynamicArrayType!T)
124
+ {
125
+ VALUE ary = rb_ary_new_capa(cast(C_long) t.length);
126
+ foreach (ref e; t)
127
+ rb_ary_push(ary, to_ruby_value(e));
128
+ return ary;
129
+ }
130
+
131
+ else static if (isStaticArrayType!T)
132
+ {
133
+ VALUE ary = rb_ary_new_capa(cast(C_long) t.length);
134
+ foreach (ref e; t)
135
+ rb_ary_push(ary, to_ruby_value(e));
136
+ return ary;
137
+ }
138
+
139
+ else static if (isAssocArrayType!T)
140
+ {
141
+ VALUE hash = rb_hash_new();
142
+ foreach (k, ref v; t)
143
+ rb_hash_aset(hash, to_ruby_value(k), to_ruby_value(v));
144
+ return hash;
145
+ }
146
+
147
+ else static assert(0, "to_ruby_value: no conversion for " ~ T.stringof);
148
+ }
149
+
150
+
151
+ // ---------------------------------------------------------------- d_type
152
+
153
+ /// Convert a Ruby VALUE into a native D type T.
154
+ ///
155
+ /// Numeric decode order matters: EXACT types must be tested before the
156
+ /// subtype branches they would otherwise fall into — is(T : C_longlong)
157
+ /// matches int too, and the old chain let `int` decode through the
158
+ /// unchecked-wide path. Each exact type now gets Ruby's own
159
+ /// range-checked converter: rb_num2int/rb_num2uint raise RangeError on
160
+ /// overflow (finding #13: silent wraparound before), fractional floats
161
+ /// truncate toward zero and NaN/inf raise FloatDomainError — exactly
162
+ /// NUM2INT/NUM2UINT/Integer() semantics (finding #14).
163
+ T d_type(T)(VALUE val)
164
+ {
165
+ static if (is(T == bool))
166
+ {
167
+ // RUBY_TEST (RTEST) semantics: nil/false are false, everything
168
+ // else — 0, 0.0, "" — is true, matching Ruby's own truthiness.
169
+ // (The old `val == Qtrue_` made Ruby truthy 0 decode as D false.)
170
+ return val != Qnil_ && val != Qfalse_;
171
+ }
172
+ else static if (is(T == string))
173
+ {
174
+ VALUE str = rb_str_to_str(val);
175
+ auto ptr = rb_string_value_ptr(&str); // may swap str in-place
176
+ return ptr[0 .. rudy_bytesize(str)].idup; // BYTE length, not chars
177
+ // (finding #12)
178
+ }
179
+
180
+ // exact unsigned machine words first (VALUE itself is a ulong alias)
181
+ else static if (is(T == ulong) || is(T == C_ulonglong))
182
+ {
183
+ rudy_check_unsigned(val); // -1 must raise, not wrap
184
+ return cast(T) rb_num2ulong(val);
185
+ } // a ulong param still means
186
+ // "Ruby Integer -> machine word",
187
+ // NOT raw passthrough
188
+ else static if (is(T == int))
189
+ return cast(T) rb_num2int(val); // range-checked (finding #13)
190
+ else static if (is(T == uint))
191
+ {
192
+ rudy_check_unsigned(val);
193
+ return cast(T) rb_num2uint(val); // range-checked (finding #13)
194
+ }
195
+
196
+ // remaining signed subtypes (long, char, short...): wide is fine
197
+ else static if (is(T : C_longlong))
198
+ return cast(T) rb_num2ll(val);
199
+
200
+ // remaining unsigned subtypes (ubyte, ushort...): range-checked
201
+ else static if (is(T : uint))
202
+ {
203
+ rudy_check_unsigned(val);
204
+ return cast(T) rb_num2uint(val);
205
+ }
206
+
207
+ else static if (is(T : double))
208
+ return cast(T) rb_num2dbl(val);
209
+
210
+ else static assert(0, "d_type: no conversion to " ~ T.stringof);
211
+ }
212
+
213
+ /// NB: D's uninitialized `double x;` is NaN (float.init), NOT 0.0 as in C.
214
+ /// Always explicitly initialize double accumulators: `double x = 0.0;` —
215
+ /// otherwise every sum silently becomes NaN (issue #3).
216
+
217
+ /// Raise a Ruby exception from D.
218
+ void rudy_raise(VALUE exc, string msg)
219
+ {
220
+ rb_raise(exc, "%s", toStringz(msg));
221
+ }