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,484 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rudy4
4
+ # File bodies for a scaffolded D-backed extension gem.
5
+ #
6
+ # Templates are kept as single-quoted heredocs so nothing is interpolated
7
+ # here, and placeholders are substituted in one pass by `render`. The
8
+ # placeholder words are deliberately written as single tokens
9
+ # (PROJECT_NAME) so they survive copy/paste and are obvious in the source.
10
+ module Templates
11
+ GEMSPEC = <<~'RUBY'
12
+ # frozen_string_literal: true
13
+
14
+ require_relative "lib/PROJECT_NAME/version"
15
+
16
+ Gem::Specification.new do |spec|
17
+ spec.name = "PROJECT_NAME"
18
+ spec.version = PROJECT_CONST::VERSION
19
+ spec.authors = ["AUTHOR"]
20
+ spec.email = ["EMAIL"]
21
+
22
+ spec.summary = "A D-backed Ruby native extension"
23
+ spec.description = <<~DESC
24
+ The native half of this gem is written in D and compiled by GDC
25
+ through the rudy4 library. Edit ext/PROJECT_NAME/PROJECT_NAME.d,
26
+ run `rake compile`, and call the result from Ruby.
27
+ DESC
28
+ spec.homepage = "https://github.com/tomash/rudy4"
29
+ spec.license = "MIT"
30
+
31
+ spec.required_ruby_version = ">= RUBY_REQ"
32
+
33
+ spec.files = Dir[
34
+ "lib/**/*.rb",
35
+ "ext/**/*.d",
36
+ "ext/**/*.rb",
37
+ "README.md",
38
+ ]
39
+ spec.require_paths = ["lib"]
40
+
41
+ # RubyGems runs extconf.rb (which writes a GDC Makefile) and then
42
+ # `make install`, exactly as it would for a C extension.
43
+ spec.extensions = ["ext/PROJECT_NAME/extconf.rb"]
44
+
45
+ spec.metadata["rubygems_mfa_required"] = "true"
46
+ end
47
+ RUBY
48
+
49
+ VERSION_RB = <<~'RUBY'
50
+ # frozen_string_literal: true
51
+
52
+ module PROJECT_CONST
53
+ VERSION = "0.1.0"
54
+ end
55
+ RUBY
56
+
57
+ LIB_RB = <<~'RUBY'
58
+ # lib/PROJECT_NAME.rb
59
+ #
60
+ # The Ruby half of the gem: load the compiled native extension, and add
61
+ # plain-Ruby conveniences on top of it if you want. The native code
62
+ # lives in ext/PROJECT_NAME/PROJECT_NAME.d.
63
+
64
+ require_relative "PROJECT_NAME/version"
65
+
66
+ # The native artifact is suffixed `_ext` so it does not shadow this
67
+ # file on the load path. `rake compile` (or `gem install`) installs it
68
+ # next to this one.
69
+ begin
70
+ require "PROJECT_NAME_ext"
71
+ rescue LoadError => e
72
+ raise LoadError, <<~MSG
73
+ Could not load the native PROJECT_NAME extension (#{e.message}).
74
+
75
+ Build it first:
76
+ rake compile
77
+
78
+ If that fails, run `rudy4 doctor` to check your Ruby + GDC setup.
79
+ MSG
80
+ end
81
+
82
+ module PROJECT_CONST
83
+ # Plain-Ruby helpers go here; `add`, `greet`, `is_even` and
84
+ # `scale_all` come from D. Anything you write here is ordinary Ruby.
85
+ end
86
+ RUBY
87
+
88
+ D_SOURCE = <<~'D'
89
+ /// PROJECT_NAME.d — the native half of the PROJECT_NAME gem.
90
+ ///
91
+ /// This is YOUR code. Everything else under ext/PROJECT_NAME/ is the
92
+ /// vendored rudy4 library (rudy/) plus the build glue (extconf.rb).
93
+ ///
94
+ /// `def!` registers a D function as a Ruby method and converts
95
+ /// arguments and return values for you. Declare plain D types:
96
+ ///
97
+ /// long <-> Integer double <-> Float
98
+ /// string <-> String bool <-> true / false
99
+ /// double[] <-> Array of Float (as a RETURN type)
100
+ /// Rest <-> Ruby *args KwArgs <-> Ruby **kwargs
101
+ ///
102
+ /// The edit loop:
103
+ ///
104
+ /// 1. change a function below
105
+ /// 2. run `rake compile` (GDC builds the .so)
106
+ /// 3. run `rake test`
107
+ ///
108
+ /// If you want a raw Ruby VALUE instead of a converted argument,
109
+ /// declare the parameter (or return type) as Raw!VALUE.
110
+
111
+ module PROJECT_NAME;
112
+
113
+ import rudy.bind.value;
114
+ import rudy.convert;
115
+ import rudy.defs;
116
+ import rudy.args;
117
+
118
+ import core.runtime : rt_init;
119
+
120
+ // ---------------------------------------------------------------------
121
+ // Your functions. Change these.
122
+ // ---------------------------------------------------------------------
123
+
124
+ /// PROJECT_CONST.add(2, 3) => 5
125
+ long add(long a, long b)
126
+ {
127
+ return a + b;
128
+ }
129
+
130
+ /// PROJECT_CONST.greet("world") => "hello, world!"
131
+ /// Strings are copied into D (idup) before your code sees them.
132
+ string greet(string name)
133
+ {
134
+ return "hello, " ~ name ~ "!";
135
+ }
136
+
137
+ /// PROJECT_CONST.is_even(4) => true
138
+ /// A D bool comes back as Ruby true / false, never a raw 0/1.
139
+ bool is_even(long n)
140
+ {
141
+ return (n % 2) == 0;
142
+ }
143
+
144
+ /// A parameter with a D default value becomes an OPTIONAL Ruby
145
+ /// argument: PROJECT_CONST.shout("hi") and PROJECT_CONST.shout("hi", 2)
146
+ /// both work.
147
+ string shout(string word, long times = 3)
148
+ {
149
+ string out_;
150
+ foreach (_; 0 .. times)
151
+ out_ ~= word ~ "!";
152
+ return out_;
153
+ }
154
+
155
+ /// PROJECT_CONST.sum_all(1, 2, 3) => 6
156
+ /// `Rest` collects Ruby's trailing arguments (*args). Values arrive as
157
+ /// raw VALUEs; convert each with d_type!T.
158
+ long sum_all(Rest rest)
159
+ {
160
+ long total = 0;
161
+ foreach (v; rest)
162
+ total += d_type!long(v);
163
+ return total;
164
+ }
165
+
166
+ /// PROJECT_CONST.squares(3) => [0.0, 1.0, 4.0]
167
+ /// A D dynamic array RETURN becomes a Ruby Array, element by element.
168
+ double[] squares(long n)
169
+ {
170
+ double[] out_;
171
+ out_.reserve(n);
172
+ foreach (i; 0 .. n)
173
+ out_ ~= cast(double) i * cast(double) i;
174
+ return out_;
175
+ }
176
+
177
+ // ---------------------------------------------------------------------
178
+ // Optional next steps (delete when you have your own code):
179
+ //
180
+ // * Named arguments: `string tag(KwArgs kw)` plus
181
+ // `kw.get!string("label", "default")` — Ruby calls it as
182
+ // PROJECT_CONST.tag(label: "x"). See README.md.
183
+ //
184
+ // * A D struct as a real Ruby object (allocate / dup / == / hash /
185
+ // inspect / freeze-aware mutators). See README.md, "Exposing a D
186
+ // struct", or examples/hello_gem in the rudy4 repo.
187
+ //
188
+ // * Long-running work that must not block other Ruby threads: run it
189
+ // under rb_thread_call_without_gvl (README.md, "Long-running work").
190
+ // ---------------------------------------------------------------------
191
+
192
+ // ---------------------------------------------------------------------
193
+ // Entry point — Ruby calls this once when the extension is required.
194
+ // Add a `def!(...)` line here for every function Ruby should see.
195
+ // ---------------------------------------------------------------------
196
+
197
+ pragma(mangle, "Init_PROJECT_NAME_ext")
198
+ extern (C) void Init_PROJECT_NAME_ext()
199
+ {
200
+ rt_init();
201
+
202
+ rb_define_module("PROJECT_CONST");
203
+
204
+ def!("PROJECT_CONST", add);
205
+ def!("PROJECT_CONST", greet);
206
+ def!("PROJECT_CONST", is_even);
207
+ def!("PROJECT_CONST", shout);
208
+ def!("PROJECT_CONST", sum_all);
209
+ def!("PROJECT_CONST", squares);
210
+ }
211
+ D
212
+
213
+ EXTCONF_RB = <<~'RUBY'
214
+ # ext/PROJECT_NAME/extconf.rb
215
+ #
216
+ # A stand-in for mkmf, for an extension whose compiler is GDC (D), not a
217
+ # C compiler. It writes a Makefile that compiles
218
+ #
219
+ # ext/PROJECT_NAME/PROJECT_NAME.d (your code)
220
+ # + ext/PROJECT_NAME/rudy/**/*.d (the vendored rudy4 library,
221
+ # shipped here so the extension
222
+ # builds with no external checkout)
223
+ #
224
+ # into PROJECT_NAME_ext.so, and exposes the `install` target RubyGems
225
+ # calls.
226
+
227
+ require "rbconfig"
228
+
229
+ HERE = __dir__
230
+ RUDY_SRC = File.join(HERE, "rudy")
231
+
232
+ unless File.file?(File.join(RUDY_SRC, "defs.d"))
233
+ abort <<~MSG
234
+ The vendored rudy4 sources are missing from #{RUDY_SRC}.
235
+ Re-run `rudy4 init PROJECT_NAME` to regenerate this project.
236
+ MSG
237
+ end
238
+
239
+ RUDY_D_SOURCES = Dir.glob(File.join(RUDY_SRC, "**", "*.d")).sort
240
+
241
+ # RubyGems sets RUBYARCHDIR for `gem install`; a bare
242
+ # `ruby extconf.rb && make install` defaults to the gem's lib/, so the
243
+ # extension can be required in place.
244
+ RUBYARCHDIR = ENV["RUBYARCHDIR"] || File.expand_path("../../lib", HERE)
245
+
246
+ hdr = RbConfig::CONFIG["rubyhdrdir"]
247
+ archhdr = RbConfig::CONFIG["rubyarchhdrdir"]
248
+ libarg = RbConfig::CONFIG["LIBRUBYARG_SHARED"]
249
+ libdir = RbConfig::CONFIG["libdir"]
250
+ dllib = RbConfig::CONFIG["DLEXT"] # "so"
251
+
252
+ gdc = ENV["GDC"] || "gdc-14"
253
+
254
+ File.write(File.join(HERE, "Makefile"), <<~MAKE)
255
+ # Generated by extconf.rb — do not edit; re-run `ruby extconf.rb`.
256
+ GDC = #{gdc}
257
+ RUBYHDR = #{hdr}
258
+ RUBYARCHHDR = #{archhdr}
259
+ LIBRUBYARG = #{libarg}
260
+ LIBRUBY_DIR = #{libdir}
261
+ RUBYARCHDIR = #{RUBYARCHDIR}
262
+
263
+ DFLAGS = -fPIC -I#{HERE} -I$(RUBYHDR) -I$(RUBYARCHHDR) \\
264
+ -fpreview=dip1008 -Wno-deprecated
265
+
266
+ RUDY_SOURCES = #{RUDY_D_SOURCES.join(" ")}
267
+
268
+ TARGET = PROJECT_NAME_ext.#{dllib}
269
+
270
+ all: $(TARGET)
271
+
272
+ $(TARGET): PROJECT_NAME.d $(RUDY_SOURCES)
273
+ \t$(GDC) -shared -o $@ $(DFLAGS) $(RUDY_SOURCES) PROJECT_NAME.d \\
274
+ \t -L$(LIBRUBY_DIR) $(LIBRUBYARG) -lm
275
+
276
+ install: $(TARGET)
277
+ \tmkdir -p $(RUBYARCHDIR)
278
+ \tcp $(TARGET) $(RUBYARCHDIR)/
279
+
280
+ clean:
281
+ \trm -f $(TARGET)
282
+ MAKE
283
+ RUBY
284
+
285
+ TEST_RB = <<~'RUBY'
286
+ # frozen_string_literal: true
287
+
288
+ # Plain-Ruby test — no test framework needed.
289
+ # Run: rake test (or: ruby -Ilib test/PROJECT_NAME_test.rb)
290
+
291
+ require "PROJECT_NAME"
292
+
293
+ checks = 0
294
+ assert = lambda do |cond, msg|
295
+ checks += 1
296
+ raise "FAIL: #{msg}" unless cond
297
+ end
298
+
299
+ assert.(PROJECT_CONST.add(2, 3) == 5, "add")
300
+ assert.(PROJECT_CONST.greet("world") == "hello, world!", "greet")
301
+ assert.(PROJECT_CONST.is_even(4) == true, "is_even(4)")
302
+ assert.(PROJECT_CONST.is_even(5) == false, "is_even(5)")
303
+ assert.(PROJECT_CONST.shout("hi", 2) == "hi!hi!", "optional argument")
304
+ assert.(PROJECT_CONST.sum_all(1, 2, 3) == 6, "rest arguments (*args)")
305
+ assert.(PROJECT_CONST.squares(3) == [0.0, 1.0, 4.0], "array return")
306
+
307
+ puts "#{checks} assertions OK"
308
+ RUBY
309
+
310
+ RAKEFILE = <<~'RUBY'
311
+ # frozen_string_literal: true
312
+
313
+ require "rake/clean"
314
+ require "rbconfig"
315
+
316
+ ROOT = __dir__
317
+ EXT_DIR = File.join(ROOT, "ext", "PROJECT_NAME")
318
+ DLEXT = RbConfig::CONFIG["DLEXT"]
319
+ TARGET = File.join(EXT_DIR, "PROJECT_NAME_ext.#{DLEXT}")
320
+ LIB_SO = File.join(ROOT, "lib", "PROJECT_NAME_ext.#{DLEXT}")
321
+
322
+ CLEAN.include(File.join(EXT_DIR, "Makefile"), TARGET, LIB_SO)
323
+ CLOBBER.include(File.join(EXT_DIR, "*.o"))
324
+
325
+ # The Ruby running this Rakefile — not whatever `ruby` is on PATH.
326
+ RUBY = RbConfig.ruby
327
+
328
+ desc "Compile the D native extension with GDC"
329
+ task :compile do
330
+ Dir.chdir(EXT_DIR) do
331
+ sh RUBY, "extconf.rb"
332
+ sh "make"
333
+ end
334
+ cp TARGET, LIB_SO
335
+ puts "built #{LIB_SO}"
336
+ end
337
+
338
+ desc "Compile, then run the test suite"
339
+ task :test => :compile do
340
+ sh RUBY, "-Ilib", "test/PROJECT_NAME_test.rb"
341
+ end
342
+
343
+ task :default => :test
344
+ RUBY
345
+
346
+ GITIGNORE = <<~'TEXT'
347
+ /lib/*.so
348
+ /lib/*.bundle
349
+ /ext/PROJECT_NAME/Makefile
350
+ /ext/PROJECT_NAME/*.so
351
+ /ext/PROJECT_NAME/*.o
352
+ /*.gem
353
+ /tmp/
354
+ TEXT
355
+
356
+ README = <<~'TEXT'
357
+ # PROJECT_NAME
358
+
359
+ A Ruby native extension whose native half is written in **D** and
360
+ compiled by **GDC** through [rudy4](https://github.com/tomash/rudy4).
361
+
362
+ ## Quickstart
363
+
364
+ ```sh
365
+ rake compile # builds ext/PROJECT_NAME/PROJECT_NAME.d
366
+ rake test # runs test/PROJECT_NAME_test.rb
367
+ ruby -Ilib -e 'require "PROJECT_NAME"; p PROJECT_CONST.add(2, 3)'
368
+ ```
369
+
370
+ ## The edit loop
371
+
372
+ 1. Edit `ext/PROJECT_NAME/PROJECT_NAME.d` — that is the whole native
373
+ side. `def!` turns a D function into a Ruby method and converts
374
+ arguments and return values:
375
+
376
+ ```d
377
+ long add(long a, long b) { return a + b; }
378
+ // ...
379
+ def!("PROJECT_CONST", add);
380
+ ```
381
+
382
+ The generated file also shows optional arguments (D default values),
383
+ Ruby `*args` (the `Rest` wrapper) and array returns — delete them
384
+ once you have your own functions.
385
+
386
+ 2. `rake compile`
387
+ 3. `rake test`
388
+
389
+ ## Layout
390
+
391
+ ```
392
+ PROJECT_NAME/
393
+ PROJECT_NAME.gemspec
394
+ Rakefile
395
+ ext/PROJECT_NAME/
396
+ PROJECT_NAME.d <- your D code
397
+ extconf.rb <- writes the GDC Makefile (mkmf stand-in)
398
+ rudy/ <- vendored rudy4 library (do not edit)
399
+ lib/
400
+ PROJECT_NAME.rb
401
+ PROJECT_NAME/version.rb
402
+ test/PROJECT_NAME_test.rb
403
+ ```
404
+
405
+ The `rudy/` tree is a copy of rudy4's D library, shipped inside the gem
406
+ so the extension builds on its own — no rudy4 checkout, no
407
+ `RUDY4_ROOT`. It is pinned to the rudy4 gem version this project was
408
+ generated with; refresh it after upgrading rudy4 (the generator prints
409
+ the exact command, or use `rudy4 vendor`).
410
+
411
+ ## Using it from another project
412
+
413
+ From the app that wants the extension:
414
+
415
+ ```ruby
416
+ # Gemfile
417
+ gem "PROJECT_NAME", path: "../PROJECT_NAME"
418
+ ```
419
+
420
+ or, once built, load it by hand:
421
+
422
+ ```sh
423
+ ruby -I../PROJECT_NAME/lib -e 'require "PROJECT_NAME"'
424
+ ```
425
+
426
+ To publish it, `gem build PROJECT_NAME.gemspec` and push it like any
427
+ other gem — RubyGems will compile the D source during install.
428
+
429
+ ## Exposing a D struct
430
+
431
+ When scalars are not enough, a D struct can become a first-class Ruby
432
+ object:
433
+
434
+ ```d
435
+ struct Vec2 { double x, y; }
436
+
437
+ mixin RudyTypedData!(Vec2, "PROJECT_NAME_vec2");
438
+ mixin RudyObjectProtocol!(Vec2, "PROJECT_NAME_vec2",
439
+ null, null, null, vec2_inspect);
440
+ ```
441
+
442
+ and in `Init_...`:
443
+
444
+ ```d
445
+ VALUE c = rb_define_class("Vec2", rb_cObject);
446
+ PROJECT_NAME_vec2_proto_install(c);
447
+ ```
448
+
449
+ That gives `Vec2` allocate / `dup` / `clone` / `==` / `eql?` / `hash` /
450
+ `inspect`; add methods of your own with `rb_define_method`. See
451
+ `examples/hello_gem/` in the rudy4 repo for a complete worked example
452
+ (including a freeze-aware `[]=`).
453
+
454
+ ## Long-running work
455
+
456
+ A `def!`'d method holds the GVL, so a slow one blocks every other Ruby
457
+ thread until it returns. For blocking I/O or long computation, wrap the
458
+ work in `rb_thread_call_without_gvl` (bound by rudy4) and provide an
459
+ unblock function so the call stays interruptible.
460
+
461
+ ## When something goes wrong
462
+
463
+ * `rudy4 doctor` — checks GDC, Ruby headers, and that a scaffolded
464
+ extension compiles and loads.
465
+ * `rake compile` fails immediately with "vendored rudy4 sources are
466
+ missing" — regenerate the project with `rudy4 init`.
467
+ * `LoadError: Could not load the native ... extension` — you have not
468
+ compiled yet (`rake compile`).
469
+
470
+ ## Requirements
471
+
472
+ * Ruby RUBY_REQ+ with development headers (`ruby-dev` / `ruby-devel`)
473
+ * GDC (the D compiler), version 13 or newer — currently tested with
474
+ GDC 14
475
+ * `make`
476
+ TEXT
477
+
478
+ def self.render(template, tokens)
479
+ out = template.dup
480
+ tokens.each { |key, value| out = out.gsub(key, value.to_s) }
481
+ out
482
+ end
483
+ end
484
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rudy4
4
+ # The vendored copy of the rudy4 D library.
5
+ #
6
+ # rudy4 is consumed as *source* (`src/rudy/**/*.d`), not as an installed
7
+ # binary. For a scaffolded project to build without a rudy4 checkout, this
8
+ # gem ships that source tree under `vendor/` and copies it into the generated
9
+ # project. `scripts/vendor_sync.sh` keeps it in sync with `src/rudy/` (CI
10
+ # fails if the two drift).
11
+ module Vendor
12
+ module_function
13
+
14
+ # Absolute path to the gem's `vendor/` directory. This is what goes on the
15
+ # GDC include path: it is the parent of the `rudy/` module directory, so
16
+ # `import rudy.bind.value;` resolves.
17
+ def root
18
+ File.expand_path("../../vendor", __dir__)
19
+ end
20
+
21
+ # Absolute path to the vendored `rudy/` module directory itself.
22
+ def rudy_dir
23
+ File.join(root, "rudy")
24
+ end
25
+
26
+ # Every vendored D source, sorted (stable order keeps build lines tidy).
27
+ def d_sources
28
+ Dir.glob(File.join(rudy_dir, "**", "*.d")).sort
29
+ end
30
+
31
+ # True when the vendored library looks complete.
32
+ def present?
33
+ File.file?(File.join(rudy_dir, "defs.d"))
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rudy4
4
+ VERSION = "0.2.0"
5
+ end
data/lib/rudy4.rb ADDED
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "rudy4/error"
4
+ require_relative "rudy4/version"
5
+ require_relative "rudy4/vendor"
6
+ require_relative "rudy4/templates"
7
+ require_relative "rudy4/generator"
8
+ require_relative "rudy4/doctor"
9
+ require_relative "rudy4/cli"
10
+
11
+ # rudy4 — write Ruby native extensions in D.
12
+ #
13
+ # This file is the library half of the `rudy4` gem. The command-line half lives
14
+ # in `bin/rudy4`, which delegates to Rudy4::CLI.
15
+ module Rudy4
16
+ end
@@ -0,0 +1,64 @@
1
+ #!/bin/bash
2
+ # ABI conformance audit for rudy's hand-written Ruby C API bindings.
3
+ #
4
+ # Compares the extern(C) declarations in src/rudy/bind/value.d against the
5
+ # symbols actually exported by libruby:
6
+ # 1. BOUND-AND-EXPORTED — declaration resolves to a real symbol
7
+ # 2. BOUND-NOT-EXPORTED — declaration with no matching symbol (MISBIND
8
+ # candidates: typos, renamed/removed APIs)
9
+ # 3. EXPORTED-NOT-BOUND — exported rb_* API surface we have not bound
10
+ # (the unbound ledger; informative, not a failure)
11
+ #
12
+ # Symbol *names* are checked; widths/signatures (the rb_int2inum int-vs-long
13
+ # bug class) must be verified against ruby.h by hand — nm cannot see types.
14
+ #
15
+ # Usage: scripts/abi_audit.sh [path-to-ruby]
16
+ set -u
17
+
18
+ RUBY="${1:-ruby}"
19
+ BINDING=src/rudy/bind/value.d
20
+ OUT=.build/abi_audit
21
+ mkdir -p "$OUT"
22
+
23
+ LIBDIR=$("$RUBY" -e 'print RbConfig::CONFIG["libdir"]')
24
+ LIBRUBY=$(ls "$LIBDIR"/libruby*.so* 2>/dev/null | head -1)
25
+ if [ -z "$LIBRUBY" ]; then
26
+ echo "ABI AUDIT FAIL: cannot locate libruby in $LIBDIR"
27
+ exit 1
28
+ fi
29
+ echo "libruby: $LIBRUBY ($("$RUBY" -v))"
30
+
31
+ # Symbols bound in value.d: function decls (name directly before '(') and
32
+ # bound globals ('extern __gshared VALUE rb_...;'). Strip trailing '2'
33
+ # suffixes? No — rb_*2 are distinct real symbols; keep verbatim.
34
+ grep -vE '^\s*//' "$BINDING" | grep -oE 'rb_[a-z_0-9]+\(' \
35
+ | sed 's/($//; s/(//' | sort -u > "$OUT/bound.txt"
36
+ grep -oE 'extern __gshared VALUE (rb_[a-zA-Z_0-9]+)' "$BINDING" \
37
+ | sed 's/extern __gshared VALUE //' | sort -u >> "$OUT/bound.txt"
38
+ sort -u "$OUT/bound.txt" -o "$OUT/bound.txt"
39
+
40
+ # Symbols exported by libruby (defined only). Ruby 4 exports both rb_* API
41
+ # and internal ruby_* — we only audit the public rb_* surface here.
42
+ nm -D --defined-only "$LIBRUBY" 2>/dev/null \
43
+ | awk '{print $NF}' | grep -E '^rb_[a-zA-Z_0-9]+$' | sort -u > "$OUT/exported.txt"
44
+
45
+ # --- report -------------------------------------------------------------
46
+ BOUND=$(wc -l < "$OUT/bound.txt")
47
+ echo "---"
48
+ echo "BOUND-AND-EXPORTED (resolved):"
49
+ comm -12 "$OUT/bound.txt" "$OUT/exported.txt" | tee "$OUT/ok.txt" | wc -l | xargs echo " count:"
50
+ echo "BOUND-NOT-EXPORTED (MISBIND candidates):"
51
+ comm -23 "$OUT/bound.txt" "$OUT/exported.txt" | tee "$OUT/misbind.txt"
52
+ MIS=$(wc -l < "$OUT/misbind.txt")
53
+ echo " count: $MIS"
54
+ echo "EXPORTED-NOT-BOUND (unbound ledger, top 60 of $(grep -cv '^$' "$OUT/exported.txt")):"
55
+ comm -13 "$OUT/bound.txt" "$OUT/exported.txt" > "$OUT/unbound.txt"
56
+ grep -cv '^$' "$OUT/unbound.txt" | xargs echo " total unbound:"
57
+ head -60 "$OUT/unbound.txt"
58
+
59
+ echo "---"
60
+ if [ "$MIS" -gt 0 ]; then
61
+ echo "ABI AUDIT FAIL: $MIS bound symbol(s) not exported by libruby ($BINDING vs $LIBRUBY)"
62
+ exit 1
63
+ fi
64
+ echo "ABI AUDIT PASS: all $BOUND bound symbols resolve in libruby"
@@ -0,0 +1,77 @@
1
+ #!/bin/bash
2
+ # scripts/memcheck.sh — ASan/valgrind torture runs for rudy4 (backlog item 7).
3
+ #
4
+ # Builds the rudy library + dextest.so + the rt_init-free experiment .so's
5
+ # with -fsanitize=address into .build.asan/, then runs the corresponding
6
+ # test suites under ruby with LD_PRELOAD=libasan.so.
7
+ #
8
+ # Usage: bash scripts/memcheck.sh
9
+ #
10
+ # Findings driving the scope (experiments/NOTES.md #23/#24):
11
+ # - gdc-14 + ASan: any explicit druntime rt_init() call SEGVs (ModuleInfo
12
+ # NULL read), independent of ASAN_OPTIONS. Experiments whose init hook
13
+ # calls rt_init (gcstorm/compact/convert/objproto) cannot run under ASan
14
+ # in this toolchain — excluded.
15
+ # - exp_http SEGVs with an ASan "Failed to munmap" even with GC disabled —
16
+ # under investigation; excluded from the run but still built.
17
+ # - LeakSanitizer is disabled: it flags D GC heap pages as leaks (false
18
+ # positives for a GC'd language).
19
+ # - valgrind is not installed on this host; CI may add it later.
20
+ set -u
21
+ cd "$(dirname "$0")/.."
22
+
23
+ RUBY_BIN="${RUBY_BIN:-$HOME/.asdf/installs/ruby/4.0.7/bin/ruby}"
24
+ GDC="${GDC:-gdc-14}"
25
+
26
+ RH=$("$RUBY_BIN" -e 'print RbConfig::CONFIG["rubyhdrdir"]')
27
+ AH=$("$RUBY_BIN" -e 'print RbConfig::CONFIG["rubyarchhdrdir"]')
28
+ LIBDIR=$("$RUBY_BIN" -e 'print RbConfig::CONFIG["libdir"]')
29
+ LIBRUBY=$("$RUBY_BIN" -e 'print RbConfig::CONFIG["LIBRUBYARG_SHARED"]')
30
+ ASAN_SO="$("$GDC" -print-file-name=libasan.so)"
31
+
32
+ BUILD_DIR="${RUDY_BUILD_DIR:-.build.asan}"
33
+ export RUDY_BUILD_DIR="$BUILD_DIR"
34
+ DFLAGS="-fsanitize=address -fPIC -g -Isrc -I$RH -I$AH -fpreview=dip1008 -Wno-deprecated"
35
+ export ASAN_OPTIONS="${ASAN_OPTIONS:-detect_leaks=0:halt_on_error=1:allocator_may_return_null=1:detect_odr_violation=0}"
36
+ export LD_PRELOAD="$ASAN_SO"
37
+
38
+ # rt_init-free experiments: buildable and runnable under ASan (http: build
39
+ # only — munmap crash under investigation, NOTES.md #24).
40
+ BUILDABLE=(wrap cross perf exc defargs http)
41
+ RUNNABLE=(wrap cross perf exc defargs)
42
+
43
+ mkdir -p "$BUILD_DIR"
44
+ OBJS=""
45
+ for f in src/rudy/*.d src/rudy/bind/*.d; do
46
+ o="$BUILD_DIR/$(echo "$f" | tr '/' '_' | sed 's/\.d$/.o/')"
47
+ "$GDC" $DFLAGS -c "$f" -o "$o" || { echo "COMPILE FAIL $f"; exit 1; }
48
+ OBJS="$OBJS $o"
49
+ done
50
+ "$GDC" $DFLAGS -shared -o "$BUILD_DIR/dextest.so" $OBJS src/dextest.d \
51
+ -L"$LIBDIR" "$LIBRUBY" -lm || { echo "COMPILE FAIL dextest.so"; exit 1; }
52
+ echo "built library objects + dextest.so into $BUILD_DIR"
53
+
54
+ rc_all=0
55
+ for s in "${BUILDABLE[@]}"; do
56
+ "$GDC" $DFLAGS -shared -o "$BUILD_DIR/exp_${s}.so" $OBJS "experiments/exp_${s}.d" \
57
+ -L"$LIBDIR" "$LIBRUBY" -lm || { echo "BUILD FAIL exp_${s}.so"; rc_all=1; continue; }
58
+ done
59
+ echo "built experiment .so's: ${BUILDABLE[*]}"
60
+
61
+ for s in "${RUNNABLE[@]}"; do
62
+ test_rb="experiments/test_exp_${s}.rb"
63
+ [ -f "$test_rb" ] || { echo "SKIP $s (no suite file)"; continue; }
64
+ echo "== ASan: $s"
65
+ "$RUBY_BIN" -Ilib "$test_rb" >"/tmp/memcheck_${s}.log" 2>&1
66
+ rc=$?
67
+ if [ $rc -eq 0 ]; then
68
+ echo " PASS (log /tmp/memcheck_${s}.log)"
69
+ else
70
+ echo " FAIL rc=$rc (log /tmp/memcheck_${s}.log)"
71
+ tail -8 "/tmp/memcheck_${s}.log"
72
+ rc_all=1
73
+ fi
74
+ done
75
+ unset LD_PRELOAD
76
+ echo "memcheck.sh rc=$rc_all"
77
+ exit $rc_all