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,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
|
+
}
|
metadata
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: rudy4
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.2.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Tomasz Stachewicz
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-10-13 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: |
|
|
14
|
+
rudy4 lets you write Ruby native extensions in D (compiled by GDC) instead
|
|
15
|
+
of C. This gem ships the rudy4 D library plus the `rudy4` command: `rudy4
|
|
16
|
+
init NAME` scaffolds a self-contained extension gem (vendoring the D
|
|
17
|
+
sources, so it builds with no checkout), and `rudy4 doctor` checks your
|
|
18
|
+
Ruby + GDC setup by compiling and loading a probe extension.
|
|
19
|
+
email:
|
|
20
|
+
- tomasz.stachewicz@gmail.com
|
|
21
|
+
executables:
|
|
22
|
+
- rudy4
|
|
23
|
+
extensions: []
|
|
24
|
+
extra_rdoc_files: []
|
|
25
|
+
files:
|
|
26
|
+
- CHANGELOG.md
|
|
27
|
+
- LICENSE
|
|
28
|
+
- Makefile
|
|
29
|
+
- README.md
|
|
30
|
+
- bin/rudy4
|
|
31
|
+
- examples/hello_gem/README.md
|
|
32
|
+
- examples/hello_gem/ext/hello_gem/extconf.rb
|
|
33
|
+
- examples/hello_gem/ext/hello_gem/hello_gem.d
|
|
34
|
+
- examples/hello_gem/hello_gem.gemspec
|
|
35
|
+
- examples/hello_gem/lib/hello_gem.rb
|
|
36
|
+
- examples/hello_gem/lib/hello_gem/version.rb
|
|
37
|
+
- examples/hello_gem/test/smoke.sh
|
|
38
|
+
- experiments/exp_compact.d
|
|
39
|
+
- experiments/exp_convert.d
|
|
40
|
+
- experiments/exp_cross.d
|
|
41
|
+
- experiments/exp_defargs.d
|
|
42
|
+
- experiments/exp_exc.d
|
|
43
|
+
- experiments/exp_gcstorm.d
|
|
44
|
+
- experiments/exp_http.d
|
|
45
|
+
- experiments/exp_nonregression.d
|
|
46
|
+
- experiments/exp_objproto.d
|
|
47
|
+
- experiments/exp_perf.d
|
|
48
|
+
- experiments/exp_wrap.d
|
|
49
|
+
- lib/rudy4.rb
|
|
50
|
+
- lib/rudy4/cli.rb
|
|
51
|
+
- lib/rudy4/doctor.rb
|
|
52
|
+
- lib/rudy4/error.rb
|
|
53
|
+
- lib/rudy4/generator.rb
|
|
54
|
+
- lib/rudy4/templates.rb
|
|
55
|
+
- lib/rudy4/vendor.rb
|
|
56
|
+
- lib/rudy4/version.rb
|
|
57
|
+
- scripts/abi_audit.sh
|
|
58
|
+
- scripts/memcheck.sh
|
|
59
|
+
- scripts/vendor_check.sh
|
|
60
|
+
- scripts/vendor_sync.sh
|
|
61
|
+
- src/dexitest.d
|
|
62
|
+
- src/dextest.d
|
|
63
|
+
- src/dtypetest.d
|
|
64
|
+
- src/rudy/args.d
|
|
65
|
+
- src/rudy/bind/value.d
|
|
66
|
+
- src/rudy/convert.d
|
|
67
|
+
- src/rudy/defs.d
|
|
68
|
+
- src/rudy/objproto.d
|
|
69
|
+
- src/rudy/typeddata.d
|
|
70
|
+
- test/dextest_loader.rb
|
|
71
|
+
- test/exitest.rb
|
|
72
|
+
- test/rudy4_test.rb
|
|
73
|
+
- test/run.rb
|
|
74
|
+
- test/typedtest.rb
|
|
75
|
+
- vendor/rudy/args.d
|
|
76
|
+
- vendor/rudy/bind/value.d
|
|
77
|
+
- vendor/rudy/convert.d
|
|
78
|
+
- vendor/rudy/defs.d
|
|
79
|
+
- vendor/rudy/objproto.d
|
|
80
|
+
- vendor/rudy/typeddata.d
|
|
81
|
+
homepage: https://github.com/tomash/rudy4
|
|
82
|
+
licenses:
|
|
83
|
+
- MIT
|
|
84
|
+
metadata:
|
|
85
|
+
rubygems_mfa_required: 'true'
|
|
86
|
+
source_code_uri: https://github.com/tomash/rudy4
|
|
87
|
+
post_install_message:
|
|
88
|
+
rdoc_options: []
|
|
89
|
+
require_paths:
|
|
90
|
+
- lib
|
|
91
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - ">="
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '4.0'
|
|
96
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
97
|
+
requirements:
|
|
98
|
+
- - ">="
|
|
99
|
+
- !ruby/object:Gem::Version
|
|
100
|
+
version: '0'
|
|
101
|
+
requirements: []
|
|
102
|
+
rubygems_version: 3.5.22
|
|
103
|
+
signing_key:
|
|
104
|
+
specification_version: 4
|
|
105
|
+
summary: Write Ruby native extensions in D
|
|
106
|
+
test_files: []
|