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
data/bin/rudy4 ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../lib/rudy4"
5
+
6
+ exit Rudy4::CLI.new(ARGV).run
@@ -0,0 +1,91 @@
1
+ # hello_gem — a shippable D-backed Ruby extension
2
+
3
+ The rest of the rudy4 repo is *acceptance-test shaped*: `src/dextest.d` and
4
+ everything under `experiments/` exists to exercise the library, not to be
5
+ installed. This directory is the counter-example — a real gem layout you can
6
+ `gem build` / `gem install`, whose native half is written in **D** and
7
+ compiled with **GDC**.
8
+
9
+ ```
10
+ hello_gem/
11
+ hello_gem.gemspec gem metadata; declares the extconf.rb extension
12
+ lib/hello_gem.rb pure-Ruby half: loads the .so, adds conveniences
13
+ lib/hello_gem/version.rb
14
+ ext/hello_gem/hello_gem.d the native extension (def! + wrapped struct)
15
+ ext/hello_gem/extconf.rb writes a Makefile that drives gdc
16
+ test/smoke.sh one-shot build + assert script
17
+ ```
18
+
19
+ ## What the native half shows
20
+
21
+ `ext/hello_gem/hello_gem.d` uses the whole rudy4 surface in one file:
22
+
23
+ | Ruby-side | D-side |
24
+ |------------------|--------------------------------------------|
25
+ | `greet(name)` | `def!(greet)` with automatic string conversion |
26
+ | `add(a, b)` | `def!(add)` with `long` conversion |
27
+ | `is_even(n)` | `def!(is_even)` → D `bool` → Ruby `true/false` |
28
+ | `HelloGem.module_greet` | `def!("HelloGem", greet, ...)` namespaced |
29
+ | `HelloVec2#[]`, `#[]=`, `#norm2` | `RudyTypedData!(Vec2)` + raw `extern(C)` |
30
+ | `HelloVec2#dup/#clone/#==/#hash/#inspect` | `RudyObjectProtocol!(Vec2)` |
31
+ | `vec.freeze` guards `vec[0] = …` | `rb_check_frozen(self)` in the mutator |
32
+
33
+ ## Building and running
34
+
35
+ From this directory:
36
+
37
+ ```sh
38
+ # 1. compile the extension in place (leaves hello_gem_ext.so in lib/)
39
+ cd ext/hello_gem
40
+ ruby extconf.rb
41
+ make
42
+ make install RUBYARCHDIR=../../lib # or let it default
43
+
44
+ # 2. try it
45
+ cd ../..
46
+ ruby -Ilib -e 'require "hello_gem"; p greet("world"); v = HelloVec2.new(3, 4); p v.norm2; p v == v.dup'
47
+ ```
48
+
49
+ Then the gem itself:
50
+
51
+ ```sh
52
+ gem build hello_gem.gemspec
53
+ gem install hello_gem-0.1.0.gem
54
+ ruby -e 'require "hello_gem"; p add(2, 3)'
55
+ ```
56
+
57
+ `test/smoke.sh` does the build-and-assert loop in one shot (17 assertions).
58
+
59
+ ### Installing outside the repo
60
+
61
+ `gem build` / `gem install` compiles the extension in a temporary copy of
62
+ the gem, so the `../../../src/rudy` auto-detection no longer applies. When
63
+ installing from an unpacked gem (not from this checkout), point `RUDY4_ROOT`
64
+ at a rudy4 checkout:
65
+
66
+ ```sh
67
+ RUDY4_ROOT=/path/to/rudy4 GDC=gdc-14 gem install ./hello_gem-0.1.0.gem
68
+ ```
69
+
70
+ A published gem would instead vendor `src/rudy` inside the gem (or depend on
71
+ a `rudy4` gem that ships it), removing the need for `RUDY4_ROOT` entirely.
72
+
73
+ ## Requirements
74
+
75
+ - Ruby 4.0 (the suite targets the 4.0 C API),
76
+ - GDC 14 (`gdc-14`),
77
+ - the rudy4 library sources, found at `../../../src/rudy` (this checkout) or
78
+ wherever `$RUDY4_ROOT` points.
79
+
80
+ ## How the extconf.rb trick works
81
+
82
+ RubyGems' native-extension protocol is language-agnostic: it runs your
83
+ `extconf.rb`, expects it to emit a `Makefile` with `all` and `install`
84
+ targets, then runs them. `extconf.rb` here interrogates `RbConfig` for the
85
+ Ruby header dirs and `libruby` exactly like `mkmf` would, then emits a
86
+ Makefile whose `CC` is `gdc-14` and that compiles the `.d` sources alongside
87
+ `hello_gem.d`. Ruby never learns the language was D.
88
+
89
+ A gem published like this would vendor the rudy4 `src/rudy` tree into the
90
+ gem (or depend on a `rudy4` gem that ships it); the in-repo default just
91
+ walks four directories up to this checkout.
@@ -0,0 +1,75 @@
1
+ # extconf.rb — stand-in for mkmf, for an extension whose "C compiler" is GDC.
2
+ #
3
+ # RubyGems knows how to run an extconf.rb + make + make install cycle for an
4
+ # extension; it does not care that the language is D. This file writes a
5
+ # Makefile that compiles `hello_gem.d` together with the rudy4 library into
6
+ # `hello_gem.so`, then exposes an `install` target that RubyGems will call.
7
+ #
8
+ # The rudy4 D library sources are found, in order, from:
9
+ # 1. $RUDY4_ROOT/src/rudy (explicit override)
10
+ # 2. ../../../src/rudy (in-repo development, this checkout)
11
+ # A published, standalone gem would instead vendor these sources into the
12
+ # gem (or declare a dependency on a `rudy4` gem that ships them).
13
+
14
+ require "rbconfig"
15
+ require "pathname"
16
+
17
+ RUDY4_ROOT =
18
+ if ENV["RUDY4_ROOT"] && !ENV["RUDY4_ROOT"].empty?
19
+ Pathname.new(ENV["RUDY4_ROOT"])
20
+ else
21
+ # ext/hello_gem/extconf.rb -> repo root is four levels up
22
+ Pathname.new(__dir__).join("..", "..", "..", "..").expand_path
23
+ end
24
+
25
+ RUDY_SRC = RUDY4_ROOT.join("src", "rudy").expand_path
26
+ unless RUDY_SRC.join("defs.d").file?
27
+ abort <<~MSG
28
+ rudy4 library sources not found at #{RUDY_SRC}.
29
+ Set RUDY4_ROOT to a rudy4 checkout, or run extconf.rb from inside one.
30
+ MSG
31
+ end
32
+
33
+ RUDY_D_SOURCES = Dir.glob(RUDY_SRC.join("**", "*.d")).sort
34
+
35
+ # Where RubyGems wants the built extension installed. `make install` below
36
+ # honours it; a plain `ruby extconf.rb && make` leaves the .so next to the
37
+ # sources (lib/) so it can be required in place.
38
+ RUBYARCHDIR = ENV["RUBYARCHDIR"] || File.expand_path("../../lib", __dir__)
39
+
40
+ hdr = RbConfig::CONFIG["rubyhdrdir"]
41
+ archhdr = RbConfig::CONFIG["rubyarchhdrdir"]
42
+ libarg = RbConfig::CONFIG["LIBRUBYARG_SHARED"]
43
+ libdir = RbConfig::CONFIG["libdir"]
44
+ dllib = RbConfig::CONFIG["DLEXT"] # "so"
45
+
46
+ gdc = ENV["GDC"] || "gdc-14"
47
+
48
+ File.write("Makefile", <<~MAKE)
49
+ # Generated by extconf.rb — do not edit.
50
+ GDC = #{gdc}
51
+ RUBYHDR = #{hdr}
52
+ RUBYARCHHDR = #{archhdr}
53
+ LIBRUBYARG = #{libarg}
54
+ LIBRUBY_DIR = #{libdir}
55
+ RUBYARCHDIR = #{RUBYARCHDIR}
56
+
57
+ DFLAGS = -fPIC -I#{RUDY4_ROOT.join("src")} -I$(RUBYHDR) -I$(RUBYARCHHDR) -fpreview=dip1008 -Wno-deprecated
58
+
59
+ RUDY_SOURCES = #{RUDY_D_SOURCES.join(" ")}
60
+
61
+ TARGET = hello_gem_ext.#{dllib}
62
+
63
+ all: $(TARGET)
64
+
65
+ $(TARGET): hello_gem.d $(RUDY_SOURCES)
66
+ \t$(GDC) -shared -o $@ $(DFLAGS) $(RUDY_SOURCES) hello_gem.d \\
67
+ \t -L$(LIBRUBY_DIR) $(LIBRUBYARG) -lm
68
+
69
+ install: $(TARGET)
70
+ \tmkdir -p $(RUBYARCHDIR)
71
+ \tcp $(TARGET) $(RUBYARCHDIR)/
72
+
73
+ clean:
74
+ \trm -f $(TARGET)
75
+ MAKE
@@ -0,0 +1,122 @@
1
+ /// hello_gem_ext — the native half of the `hello_gem` gem (a shippable
2
+ /// D-backed Ruby extension).
3
+ ///
4
+ /// Demonstrates the whole rudy4 surface in one installable gem:
5
+ /// * `def!` for global functions and instance methods, with automatic
6
+ /// D<->Ruby conversion,
7
+ /// * `RudyTypedData` + `RudyObjectProtocol` to expose a real D struct
8
+ /// (a `Vec2`) as a first-class Ruby object (allocate/dup/clone/==/hash/
9
+ /// inspect/freeze),
10
+ /// * raw `extern(C)` C-API bodies for the methods that need them.
11
+ ///
12
+ /// Build with the gem's extconf.rb (see ../extconf.rb), which drives gdc.
13
+
14
+ module hello_gem;
15
+
16
+ import rudy.bind.value;
17
+ import rudy.convert;
18
+ import rudy.typeddata;
19
+ import rudy.objproto;
20
+ import rudy.defs;
21
+
22
+ import core.runtime : rt_init;
23
+ import std.string : format;
24
+
25
+ // ---------------------------------------------------------------------
26
+ // def!'d plain functions — conversion is automatic.
27
+ // ---------------------------------------------------------------------
28
+
29
+ string greet(string name) { return "hello, " ~ name ~ "!"; }
30
+
31
+ long add(long a, long b) { return a + b; }
32
+
33
+ bool is_even(long n) { return (n % 2) == 0; }
34
+
35
+ string describe(string what, int count)
36
+ {
37
+ import std.conv : to;
38
+ return what ~ " x" ~ count.to!string;
39
+ }
40
+
41
+ // ---------------------------------------------------------------------
42
+ // A wrapped D struct: Vec2 as a Ruby object.
43
+ // ---------------------------------------------------------------------
44
+
45
+ struct Vec2
46
+ {
47
+ double x, y;
48
+ }
49
+
50
+ mixin RudyTypedData!(Vec2, "hello_vec2");
51
+
52
+ private string vec2_inspect(ref const Vec2 v)
53
+ {
54
+ return format("x=%g y=%g", v.x, v.y);
55
+ }
56
+
57
+ // Companion protocol: allocate / dup / clone / == / eql? / hash / inspect.
58
+ mixin RudyObjectProtocol!(Vec2, "hello_vec2",
59
+ null, null, null,
60
+ vec2_inspect);
61
+
62
+ extern (C) VALUE vec2_initialize(VALUE self, VALUE x, VALUE y)
63
+ {
64
+ Vec2* p = hello_vec2_get(self);
65
+ p.x = d_type!double(x);
66
+ p.y = d_type!double(y);
67
+ return self;
68
+ }
69
+
70
+ extern (C) VALUE vec2_get_component(VALUE self, VALUE i)
71
+ {
72
+ Vec2* p = hello_vec2_get(self);
73
+ final switch (d_type!long(i)) {
74
+ case 0: return to_ruby_value(p.x);
75
+ case 1: return to_ruby_value(p.y);
76
+ }
77
+ }
78
+
79
+ extern (C) VALUE vec2_set_component(VALUE self, VALUE i, VALUE v)
80
+ {
81
+ rb_check_frozen(self); // honour Ruby's freeze contract
82
+ Vec2* p = hello_vec2_get(self);
83
+ switch (d_type!long(i)) {
84
+ case 0: p.x = d_type!double(v); break;
85
+ default: p.y = d_type!double(v); break;
86
+ }
87
+ return v;
88
+ }
89
+
90
+ extern (C) VALUE vec2_norm2(VALUE self)
91
+ {
92
+ Vec2* p = hello_vec2_get(self);
93
+ return to_ruby_value(p.x * p.x + p.y * p.y);
94
+ }
95
+
96
+ // ---------------------------------------------------------------------
97
+ // Entry point — Ruby calls this once when the extension is required.
98
+ // ---------------------------------------------------------------------
99
+
100
+ pragma(mangle, "Init_hello_gem_ext")
101
+ extern (C) void Init_hello_gem_ext()
102
+ {
103
+ rt_init();
104
+
105
+ // global functions (on Object)
106
+ def!(greet);
107
+ def!(add);
108
+ def!(is_even);
109
+ def!(describe);
110
+
111
+ // a namespaced module function
112
+ VALUE HelloGem = rb_define_module("HelloGem");
113
+ def!("HelloGem", greet, "module_greet");
114
+
115
+ // the wrapped struct as a top-level class
116
+ VALUE c = rb_define_class("HelloVec2", rb_cObject);
117
+ hello_vec2_proto_install(c);
118
+ rb_define_method(c, "initialize", cast(AnyMethod) &vec2_initialize, 2);
119
+ rb_define_method(c, "[]", cast(AnyMethod) &vec2_get_component, 1);
120
+ rb_define_method(c, "[]=", cast(AnyMethod) &vec2_set_component, 2);
121
+ rb_define_method(c, "norm2", cast(AnyMethod) &vec2_norm2, 0);
122
+ }
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/hello_gem/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "hello_gem"
7
+ spec.version = HelloGem::VERSION
8
+ spec.authors = ["Tomasz Stachewicz"]
9
+ spec.email = ["tomasz.stachewicz@gmail.com"]
10
+
11
+ spec.summary = "A D-backed Ruby extension, packaged as a gem"
12
+ spec.description = <<~DESC
13
+ Reference example for rudy4: the native half of this gem is written in D
14
+ (compiled by GDC) and exposes global functions plus a wrapped D struct to
15
+ Ruby. Shows the gem-shaped layout (gemspec + ext/ + lib/) that a real
16
+ rudy4-based extension would ship with.
17
+ DESC
18
+ spec.homepage = "https://github.com/tomash/rudy4"
19
+ spec.license = "MIT"
20
+
21
+ spec.required_ruby_version = ">= 4.0"
22
+
23
+ spec.files = Dir[
24
+ "lib/**/*.rb",
25
+ "ext/**/*.d",
26
+ "ext/**/*.rb",
27
+ "README.md",
28
+ ]
29
+ spec.require_paths = ["lib"]
30
+
31
+ # Builds hello_gem.so from D sources via GDC.
32
+ spec.extensions = ["ext/hello_gem/extconf.rb"]
33
+
34
+ spec.metadata["rubygems_mfa_required"] = "true"
35
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HelloGem
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,32 @@
1
+ # lib/hello_gem.rb
2
+ #
3
+ # Pure-Ruby half of the gem. Its job: load the compiled native extension and
4
+ # provide the conventional `HelloGem` namespace. Native code is written in D
5
+ # (see ext/hello_gem/hello_gem.d) and compiled by extconf.rb via GDC.
6
+
7
+ require "hello_gem/version"
8
+
9
+ # `hello_gem_ext.so` is installed into this directory by the gem's install
10
+ # step. (The native artifact is suffixed `_ext` so it does not collide with
11
+ # this file, `hello_gem.rb`.)
12
+ begin
13
+ require "hello_gem_ext"
14
+ rescue LoadError => e
15
+ raise LoadError, <<~MSG
16
+ Could not load the native hello_gem extension (#{e.message}).
17
+ Build it first:
18
+ cd ext/hello_gem && ruby extconf.rb && make && make install
19
+ MSG
20
+ end
21
+
22
+ module HelloGem
23
+ # A small pure-Ruby convenience on top of the native Vec2: a unit vector.
24
+ # `HelloVec2` itself comes from D land.
25
+ def self.unit(x, y)
26
+ v = HelloVec2.new(x, y)
27
+ n = Math.sqrt(v.norm2)
28
+ return v if n.zero?
29
+
30
+ HelloVec2.new(x / n, y / n)
31
+ end
32
+ end
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env bash
2
+ # smoke.sh — build the hello_gem extension from D sources and assert it works.
3
+ #
4
+ # One-shot end-to-end check for the packaging example:
5
+ # extconf.rb -> make -> require -> exercise def! functions + the wrapped
6
+ # struct's object protocol. Exits non-zero on the first failed assertion.
7
+ #
8
+ # Usage: bash test/smoke.sh [ruby-binary] (default: the rudy4 asdf ruby)
9
+
10
+ set -euo pipefail
11
+
12
+ HERE="$(cd "$(dirname "$0")/.." && pwd)"
13
+ RUBY="${1:-$HOME/.asdf/installs/ruby/4.0.7/bin/ruby}"
14
+ GDC="${GDC:-gdc-14}"
15
+
16
+ echo "== ruby: $($RUBY -v)"
17
+ echo "== gdc: $($GDC --version | head -1)"
18
+
19
+ cd "$HERE/ext/hello_gem"
20
+ rm -f hello_gem_ext.so
21
+ GDC="$GDC" "$RUBY" extconf.rb
22
+ make
23
+ make install RUBYARCHDIR="$HERE/lib"
24
+
25
+ echo "== running smoke assertions"
26
+ cd "$HERE"
27
+ "$RUBY" -Ilib - <<'RUBY'
28
+ require "hello_gem"
29
+
30
+ checks = 0
31
+ assert = lambda do |cond, msg|
32
+ checks += 1
33
+ raise "FAIL: #{msg}" unless cond
34
+ end
35
+
36
+ # def! global functions + automatic conversion
37
+ assert.(greet("world") == "hello, world!", "greet/string conversion")
38
+ assert.(add(2, 3) == 5, "add/long conversion")
39
+ assert.(is_even(4) == true, "is_even true")
40
+ assert.(is_even(5) == false, "is_even false")
41
+ assert.(describe("cat", 3) == "cat x3", "multi-arg conversion")
42
+
43
+ # namespaced module function
44
+ assert.(HelloGem.module_greet("D") == "hello, D!", "module function")
45
+
46
+ # wrapped D struct: allocate + initialize
47
+ v = HelloVec2.new(3.0, 4.0)
48
+ assert.(v[0] == 3.0 && v[1] == 4.0, "component get")
49
+ assert.(v.norm2 == 25.0, "norm2")
50
+
51
+ # object protocol
52
+ assert.(v == HelloVec2.new(3.0, 4.0), "value equality")
53
+ assert.(v.eql?(HelloVec2.new(3.0, 4.0)), "eql?")
54
+ assert.(v.hash == HelloVec2.new(3.0, 4.0).hash, "hash contract")
55
+ assert.(v.dup[0] == 3.0, "dup copies struct")
56
+ assert.(v.inspect.include?("x=3"), "inspect via hook")
57
+ h = { HelloVec2.new(1.0, 1.0) => :found }
58
+ assert.(h[HelloVec2.new(1.0, 1.0)] == :found, "hash keying")
59
+
60
+ # freeze enforcement (rb_check_frozen in the mutator)
61
+ f = HelloVec2.new(1.0, 2.0)
62
+ f.freeze
63
+ begin
64
+ f[0] = 9.0
65
+ raise "FAIL: frozen mutator did not raise"
66
+ rescue FrozenError
67
+ checks += 1
68
+ end
69
+
70
+ # mutating a copy does not touch the original
71
+ a = HelloVec2.new(1.0, 1.0)
72
+ b = a.dup
73
+ b[0] = 7.0
74
+ assert.(a[0] == 1.0 && b[0] == 7.0, "dup independence")
75
+
76
+ # pure-Ruby convenience on top of the native struct
77
+ u = HelloGem.unit(0.0, 5.0)
78
+ assert.((u.norm2 - 1.0).abs < 1e-9, "HelloGem.unit")
79
+
80
+ puts "hello_gem smoke: #{checks} assertions OK"
81
+ RUBY
@@ -0,0 +1,178 @@
1
+ /// EXPERIMENT 7 — GC compaction vs wrapped structs and registered globals.
2
+ ///
3
+ /// Torture elements:
4
+ /// - BoxCell is a wrapped D struct that HOLDS live Ruby VALUEs (a String
5
+ /// and an Array). The RudyTypedData mixin currently has NO dmark slot
6
+ /// exposed — so if Ruby's GC can't see the referents through the box,
7
+ /// they can be collected while the box survives => dangling VALUE inside
8
+ /// a live Ruby object. Then GC.compact moves survivors; stale VALUEs in
9
+ /// registered globals (and inside boxes without dcompact) point into
10
+ /// unmoved memory.
11
+ /// - globals: raw VALUEs registered via rb_gc_register_address. After
12
+ /// GC.compact, Ruby says "update your references" (rb_gc_location).
13
+ /// A D extension that ignores that keeps pre-compaction VALUEs.
14
+ ///
15
+ /// Ruby API:
16
+ /// DExpCompact.make_box(str, arr) -> DBox instance (TypedData wrap)
17
+ /// DExpCompact.read_box(obj) -> [str, arr] back out of the box
18
+ /// DExpCompact.stash(str) -> store in registered global, returns id
19
+ /// DExpCompact.peek_stashed(id) -> read back (may be stale VALUE)
20
+ /// DExpCompact.refresh(id) -> rb_gc_location refresh, returns new id
21
+ ///
22
+ /// What can go wrong (the point):
23
+ /// - box holds VALUEs, nothing marks them -> collected while box alive
24
+ /// (RUBY_T_NONE garbage / segfault on read_box)
25
+ /// - GC.compact moves referents -> box's stored VALUEs / registered
26
+ /// globals go stale; rb_gc_location is required to fix them
27
+
28
+ module exp_compact;
29
+
30
+ import rudy.bind.value;
31
+ import rudy.convert;
32
+ import rudy.typeddata;
33
+
34
+ import core.runtime : rt_init;
35
+ import core.stdc.stdlib : malloc, free;
36
+
37
+ private:
38
+
39
+ // A wrapped struct that holds LIVE Ruby VALUEs — the dangerous case.
40
+ struct BoxCell
41
+ {
42
+ VALUE str; // a live Ruby String
43
+ VALUE arr; // a live Ruby Array
44
+ ulong serial;
45
+ }
46
+
47
+ // Two descriptors for the same struct: one WITHOUT mark/compact (the old
48
+ // library behavior — kept deliberately as the negative control) and one
49
+ // WITH (finding #11 fix). The Ruby test creates both and shows that the
50
+ // unguarded box corrupts while the guarded one survives.
51
+ mixin RudyTypedData!(BoxCell, "boxcell"); // no mark (control)
52
+
53
+ extern (C) void boxcell_mark(void* p)
54
+ {
55
+ auto c = cast(BoxCell*) p;
56
+ rb_gc_mark_movable(c.str);
57
+ rb_gc_mark_movable(c.arr);
58
+ }
59
+
60
+ extern (C) void boxcell_compact(void* p)
61
+ {
62
+ auto c = cast(BoxCell*) p;
63
+ c.str = rb_gc_location(c.str);
64
+ c.arr = rb_gc_location(c.arr);
65
+ }
66
+
67
+ mixin RudyTypedData!(BoxCell, "gboxcell", &rudy_typed_free,
68
+ &boxcell_mark, &boxcell_compact); // guarded
69
+
70
+ __gshared VALUE boxcell_klass;
71
+ __gshared VALUE gboxcell_klass;
72
+
73
+ // ------------------------------------------------- registered globals
74
+
75
+ // stash slots for the rb_gc_register_address part of the torture.
76
+ // We keep a fixed pool so ids are stable and Ruby side can request refresh.
77
+ enum STASH_SIZE = 16;
78
+ __gshared VALUE[STASH_SIZE] stash;
79
+ __gshared bool[STASH_SIZE] stash_used;
80
+
81
+ extern (C) void stash_mark_all() {} // placeholder (unused)
82
+
83
+ // ---------------------------------------------------------- Ruby API
84
+
85
+ extern (C) VALUE make_box(VALUE self, VALUE str, VALUE arr)
86
+ {
87
+ auto cell = cast(BoxCell*) malloc(BoxCell.sizeof);
88
+ cell.str = str;
89
+ cell.arr = arr;
90
+ cell.serial = 0;
91
+ return boxcell_wrap(boxcell_klass, cell);
92
+ }
93
+
94
+ extern (C) VALUE make_gbox(VALUE self, VALUE str, VALUE arr)
95
+ {
96
+ auto cell = cast(BoxCell*) malloc(BoxCell.sizeof);
97
+ cell.str = str;
98
+ cell.arr = arr;
99
+ cell.serial = 0;
100
+ return gboxcell_wrap(gboxcell_klass, cell);
101
+ }
102
+
103
+ extern (C) VALUE read_box(VALUE self, VALUE obj)
104
+ {
105
+ // both classes share the struct layout and the same wrap_struct_name
106
+ // would collide — gboxcell has its own name so rb_check_typeddata
107
+ // distinguishes. We branch on the class of obj.
108
+ BoxCell* c;
109
+ if (obj_class_matches(obj, gboxcell_klass))
110
+ c = gboxcell_get(obj);
111
+ else
112
+ c = boxcell_get(obj);
113
+ VALUE[2] pair;
114
+ pair[0] = c.str;
115
+ pair[1] = c.arr;
116
+ return rb_ary_new_from_values(2, pair.ptr);
117
+ }
118
+
119
+ private bool obj_class_matches(VALUE obj, VALUE klass)
120
+ {
121
+ return rb_obj_class(obj) == klass;
122
+ }
123
+
124
+ extern (C) VALUE stash_value(VALUE self, VALUE str)
125
+ {
126
+ int id = -1;
127
+ foreach (i; 0 .. STASH_SIZE)
128
+ if (!stash_used[i]) { id = cast(int) i; break; }
129
+ if (id < 0) rb_raise(rb_eRuntimeError, "no free stash slot\0".ptr);
130
+ stash[id] = str;
131
+ stash_used[id] = true;
132
+ return to_ruby_value!int(id);
133
+ }
134
+
135
+ extern (C) VALUE peek_stashed(VALUE self, VALUE id_v)
136
+ {
137
+ int id = d_type!int(id_v);
138
+ if (id < 0 || id >= STASH_SIZE || !stash_used[id])
139
+ return Qnil_;
140
+ return stash[id];
141
+ }
142
+
143
+ extern (C) VALUE refresh(VALUE self, VALUE id_v)
144
+ {
145
+ int id = d_type!int(id_v);
146
+ if (id < 0 || id >= STASH_SIZE || !stash_used[id])
147
+ return Qnil_;
148
+ VALUE refreshed = rb_gc_location(stash[id]);
149
+ stash[id] = refreshed;
150
+ return refreshed;
151
+ }
152
+
153
+ // ---------------------------------------------------------- init
154
+
155
+ pragma(mangle, "Init_exp_compact")
156
+ extern (C) void Init_exp_compact()
157
+ {
158
+ rt_init(); // torture finding #5 (exp_gcstorm): dlopened D .so needs it
159
+
160
+ // register the stash array with Ruby's GC — Ruby now knows these
161
+ // slots hold VALUEs (it roots and updates-eligibles them at compaction
162
+ // time via rb_gc_location — which WE must call to fetch new values).
163
+ foreach (i; 0 .. STASH_SIZE)
164
+ rb_gc_register_address(&stash[i]);
165
+
166
+ VALUE c = rb_define_class("DBox", rb_cObject);
167
+ boxcell_klass = c;
168
+ VALUE gc_ = rb_define_class("DGuardedBox", rb_cObject);
169
+ gboxcell_klass = gc_;
170
+
171
+ VALUE m = rb_define_module("DExpCompact");
172
+ rb_define_module_function(m, "make_box", cast(AnyMethod) &make_box, 2);
173
+ rb_define_module_function(m, "make_gbox", cast(AnyMethod) &make_gbox, 2);
174
+ rb_define_module_function(m, "read_box", cast(AnyMethod) &read_box, 1);
175
+ rb_define_module_function(m, "stash", cast(AnyMethod) &stash_value, 1);
176
+ rb_define_module_function(m, "peek_stashed", cast(AnyMethod) &peek_stashed, 1);
177
+ rb_define_module_function(m, "refresh", cast(AnyMethod) &refresh, 1);
178
+ }