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
data/bin/rudy4
ADDED
|
@@ -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,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
|
+
}
|