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
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 5491005c45ee1bc1515fe62dd7ed87d059a4383032f2e47b0597f739c27497d3
|
|
4
|
+
data.tar.gz: 478945a8c371a446853455f049162363a3e960d3a663ea4718b09e39e7b4a648
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: b7f70024d16116739ed070e62c2988b7f45edeced059dd7669f085993bf4290b0d13bf894193206b6e508dcec6fc97c9eb2948f947923f030998601daadcc552
|
|
7
|
+
data.tar.gz: d24d6bbc27b3ff2e8ef6729474c77094cda12b04c7404cd8974c83490b3c215750b3a21fcf97a8e885fe50f77c92f9ea46435b27dea04d60f1470c75b2a91a23
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format is based
|
|
4
|
+
on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
|
|
5
|
+
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.2.0] - 2026-10-13
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **The `rudy4` gem** (`rudy4.gemspec`, `lib/rudy4/`, `bin/rudy4`) — packages
|
|
14
|
+
rudy4 as a command, not a repo to clone. The gem ships the D library under
|
|
15
|
+
`vendor/rudy/` so a generated project builds with no rudy4 checkout and no
|
|
16
|
+
`RUDY4_ROOT`.
|
|
17
|
+
- `rudy4 init NAME` scaffolds a self-contained extension gem (gemspec,
|
|
18
|
+
`lib/`, `ext/NAME/` with the *vendored* `rudy/` sources, `Rakefile`,
|
|
19
|
+
`test/`) and prints the compile/test next steps.
|
|
20
|
+
- `rudy4 doctor` verifies the Ruby toolchain, headers, shared libruby and
|
|
21
|
+
GDC — then proves the setup by scaffolding and compiling a throwaway
|
|
22
|
+
extension, so failures name the missing piece. Untested Ruby majors and
|
|
23
|
+
pre-13 GDC are warnings, not refusals.
|
|
24
|
+
- `rudy4 vendor` re-copies the D library into an existing project after a
|
|
25
|
+
gem upgrade (`src/rudy/` stays the source of truth).
|
|
26
|
+
- `scripts/vendor_sync.sh` / `scripts/vendor_check.sh` — keep `vendor/rudy/`
|
|
27
|
+
in step with `src/rudy/`; CI fails on drift.
|
|
28
|
+
- `test/rudy4_test.rb` — the gem's suite (136 assertions: scaffold contents,
|
|
29
|
+
argument validation, `--force`, vendor refresh, doctor
|
|
30
|
+
pass/fail). `RUDY4_FULL=1` additionally scaffolds a project and drives
|
|
31
|
+
`rake test` inside it with GDC (143 assertions, `make gem-test-full`).
|
|
32
|
+
- `make gem-test` / `gem-test-full` / `vendor` / `vendor-check` / `gem-build`.
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
|
|
36
|
+
- The scaffolded template no longer demonstrates array *parameters*
|
|
37
|
+
(`double[] xs`) — `d_type` has no array/AA decode path, so that template
|
|
38
|
+
would have failed to compile. The generated D file now uses the paths that
|
|
39
|
+
are actually supported: D default values for optional arguments, `Rest` for
|
|
40
|
+
Ruby `*args`, and a dynamic-array *return* (`double[] squares(long n)`),
|
|
41
|
+
alongside `long`/`string`/`bool`.
|
|
42
|
+
|
|
43
|
+
## [0.1.1] - 2026-10-13
|
|
44
|
+
|
|
45
|
+
### Added
|
|
46
|
+
|
|
47
|
+
- **`exp_nonregression`** (`experiments/exp_nonregression.d` +
|
|
48
|
+
`test_exp_nonregression.rb`, 20 assertions) — evidence for the three
|
|
49
|
+
documented non-goals: Fiber switches through D frames, Ractor
|
|
50
|
+
non-shareability (asserted VM refusal), and interruptibility of a long
|
|
51
|
+
GVL-held D call vs the GVL-released variant. Findings #25–#27 in
|
|
52
|
+
`experiments/NOTES.md`.
|
|
53
|
+
- **README "Known limitations (non-goals)"** section and a matching
|
|
54
|
+
`experiments/NOTES.md` section, so the boundaries are stated up front
|
|
55
|
+
instead of being discoverable only by reading the experiment notes.
|
|
56
|
+
|
|
57
|
+
## [0.1.0] - 2026-10-13
|
|
58
|
+
|
|
59
|
+
First tagged release: rudy4 revival targeting Ruby 4.0 and GDC 14.
|
|
60
|
+
|
|
61
|
+
### Added
|
|
62
|
+
|
|
63
|
+
- **`def!`** — register a D function as a Ruby method with automatic
|
|
64
|
+
argument conversion (`src/rudy/defs.d`, `src/rudy/convert.d`).
|
|
65
|
+
- Variadic `def!` with D default arguments → Ruby optional arguments.
|
|
66
|
+
- Blocks (`RubyBlock`), `*rest` (`Rest`), `**kwargs` (`KwArgs`),
|
|
67
|
+
`Self` receiver, `def_private!` / `def_protected!` /
|
|
68
|
+
`def_singleton!` / `def_singleton_on!`.
|
|
69
|
+
- **`RudyTypedData`** (`src/rudy/typeddata.d`) — wrap D structs as Ruby
|
|
70
|
+
objects, with GC mark + compaction callbacks (`rb_gc_mark_movable` /
|
|
71
|
+
`rb_gc_location`).
|
|
72
|
+
- **`RudyObjectProtocol`** (`src/rudy/objproto.d`) — companion mixin for
|
|
73
|
+
wrapped structs: `allocate`, `initialize_copy` (dup/clone, frozen
|
|
74
|
+
propagation), `==`/`eql?`/`hash`, `inspect`, and mutator freeze guards.
|
|
75
|
+
- **Hand-written Ruby 4.0 C-API binding** (`src/rudy/bind/value.d`),
|
|
76
|
+
~220 lines: value/type checks, strings, arrays, hashes, symbols, blocks,
|
|
77
|
+
structured exceptions (`rb_rescue`/`rb_ensure`/`rb_protect`), per-object
|
|
78
|
+
methods, GVL release (`rb_thread_call_without_gvl`).
|
|
79
|
+
- **`examples/hello_gem/`** — a complete gem-shaped extension showing how to
|
|
80
|
+
actually ship a D-backed gem: `*.gemspec`, `ext/<name>/extconf.rb`,
|
|
81
|
+
`lib/`, and packaging/installation instructions.
|
|
82
|
+
- **Experiments** (`experiments/`) — ten torture suites / 432 assertions:
|
|
83
|
+
wrap, cross, perf, http, gcstorm, exc, compact, convert, defargs,
|
|
84
|
+
objproto. Their findings are catalogued in `experiments/NOTES.md`
|
|
85
|
+
(24 findings).
|
|
86
|
+
- **CI** (`.github/workflows/ci.yml`) — GitHub Actions on ubuntu-24.04:
|
|
87
|
+
gdc-14 + Ruby 4.0.7 built from source (`--enable-shared`), runs
|
|
88
|
+
`make test`, `make exp-test`, and the ABI audit.
|
|
89
|
+
- **ABI conformance audit** (`scripts/abi_audit.sh`) — `nm -D libruby` vs
|
|
90
|
+
the `extern(C)` declarations in `src/rudy/bind/value.d`; 137 bound
|
|
91
|
+
symbols, 0 misbinds.
|
|
92
|
+
- **Memory-safety runs** (`make memcheck`, `scripts/memcheck.sh`) —
|
|
93
|
+
AddressSanitizer subset of the experiment suites (`.build.asan/`, driven
|
|
94
|
+
by `RUDY_BUILD_DIR`). Two hard gdc-14 limitations documented
|
|
95
|
+
(`experiments/NOTES.md` #23/#24).
|
|
96
|
+
- **LICENSE** (MIT) and this changelog.
|
|
97
|
+
|
|
98
|
+
### Known limitations
|
|
99
|
+
|
|
100
|
+
See `experiments/NOTES.md` for the full catalogue. Highlights:
|
|
101
|
+
|
|
102
|
+
- gdc-14 AddressSanitizer SEGVs on any explicit druntime `rt_init()` (#23);
|
|
103
|
+
only the `rt_init`-free experiments run under ASan.
|
|
104
|
+
- `exp_http` aborts under ASan with `Failed to munmap` (#24) — built but not
|
|
105
|
+
run.
|
|
106
|
+
- GDC 14 rejects `&fn` as a template value argument in hook positions —
|
|
107
|
+
pass hooks as ALIAS parameters with bare symbol names (#22).
|
|
108
|
+
- Forking after a D-runtime-initialized `.so` is loaded is unsafe — use a
|
|
109
|
+
subprocess re-exec for crash-witness tests.
|
|
110
|
+
|
|
111
|
+
[Unreleased]: https://github.com/tomash/rudy4/compare/v0.2.0...HEAD
|
|
112
|
+
[0.2.0]: https://github.com/tomash/rudy4/releases/tag/v0.2.0
|
|
113
|
+
[0.1.1]: https://github.com/tomash/rudy4/compare/v0.1.0...v0.1.1
|
|
114
|
+
[0.1.0]: https://github.com/tomash/rudy4/releases/tag/v0.1.0
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2010, 2026 Tomasz Stachewicz
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/Makefile
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# RuDy — Ruby native extensions in D (2026 revival)
|
|
2
|
+
#
|
|
3
|
+
# Toolchain: gdc-14 (GCC 14.2), Ruby 4.0 via asdf on this machine.
|
|
4
|
+
# Override on the command line if your setup differs:
|
|
5
|
+
# make RUBY=~/.asdf/installs/ruby/4.0.7/bin/ruby GDC=gdc
|
|
6
|
+
|
|
7
|
+
RUBY ?= $(HOME)/.asdf/installs/ruby/4.0.7/bin/ruby
|
|
8
|
+
GDC ?= gdc-14
|
|
9
|
+
|
|
10
|
+
RUBYHDR := $(shell $(RUBY) -e 'print RbConfig::CONFIG["rubyhdrdir"]')
|
|
11
|
+
RUBYARCHHDR := $(shell $(RUBY) -e 'print RbConfig::CONFIG["rubyarchhdrdir"]')
|
|
12
|
+
LIBRUBYARG := $(shell $(RUBY) -e 'print RbConfig::CONFIG["LIBRUBYARG_SHARED"]')
|
|
13
|
+
LIBRUBY_DIR := $(shell $(RUBY) -e 'print RbConfig::CONFIG["libdir"]')
|
|
14
|
+
|
|
15
|
+
BUILD ?= .build
|
|
16
|
+
# ASan variant: `make ASAN=1 ...` or `make memcheck` builds into .build.asan
|
|
17
|
+
# with -fsanitize=address. Note (experiments/NOTES.md #23): druntime rt_init
|
|
18
|
+
# SEGVs under gdc-14 ASan, so only the rt_init-free experiments
|
|
19
|
+
# (wrap/cross/perf/exc/defargs/http) are meaningful this way.
|
|
20
|
+
ifeq ($(ASAN),1)
|
|
21
|
+
BUILD := .build.asan
|
|
22
|
+
DFLAGS += -fsanitize=address
|
|
23
|
+
endif
|
|
24
|
+
|
|
25
|
+
DFLAGS := -fPIC -Isrc -I$(RUBYHDR) -I$(RUBYARCHHDR) \
|
|
26
|
+
-fpreview=dip1008 \
|
|
27
|
+
-Wno-deprecated
|
|
28
|
+
|
|
29
|
+
DC_SRCS := $(wildcard src/rudy/*.d src/rudy/bind/*.d)
|
|
30
|
+
|
|
31
|
+
EXP_SRCS := exp_wrap exp_cross exp_perf exp_http exp_gcstorm exp_exc exp_compact exp_convert exp_defargs exp_objproto exp_nonregression
|
|
32
|
+
|
|
33
|
+
# rt_init-free subset that works under ASan (NOTES.md #23/#24).
|
|
34
|
+
ASAN_EXP_SRCS := exp_wrap exp_cross exp_perf exp_exc exp_defargs exp_http
|
|
35
|
+
|
|
36
|
+
.PHONY: all clean test check typed-test exp exp-test exp-asan exp-asan-test memcheck vendor vendor-check gem-test gem-test-full gem-build
|
|
37
|
+
|
|
38
|
+
all: $(BUILD)/dextest.so $(BUILD)/dexitest.so $(BUILD)/dtypetest.so
|
|
39
|
+
|
|
40
|
+
# One shared object: the rudy library modules + the extension entry point.
|
|
41
|
+
$(BUILD)/dextest.so: $(DC_SRCS) src/dextest.d | $(BUILD)
|
|
42
|
+
$(GDC) -shared -o $@ $(DFLAGS) $(DC_SRCS) src/dextest.d \
|
|
43
|
+
-L$(LIBRUBY_DIR) $(LIBRUBYARG) -lm
|
|
44
|
+
|
|
45
|
+
# Missing-API bindings extension: yield/rescue/ensure/singleton/scan_args/
|
|
46
|
+
# GVL bindings + variadic def! (D default args -> Ruby optional args).
|
|
47
|
+
$(BUILD)/dexitest.so: $(DC_SRCS) src/dexitest.d | $(BUILD)
|
|
48
|
+
$(GDC) -shared -o $@ $(DFLAGS) $(DC_SRCS) src/dexitest.d \
|
|
49
|
+
-L$(LIBRUBY_DIR) $(LIBRUBYARG) -lm
|
|
50
|
+
|
|
51
|
+
# TypedData extension: rudy library + typeddata helper + dtypetest entry point.
|
|
52
|
+
$(BUILD)/dtypetest.so: $(DC_SRCS) src/dtypetest.d | $(BUILD)
|
|
53
|
+
$(GDC) -shared -o $@ $(DFLAGS) $(DC_SRCS) src/dtypetest.d \
|
|
54
|
+
-L$(LIBRUBY_DIR) $(LIBRUBYARG) -lm
|
|
55
|
+
|
|
56
|
+
$(BUILD):
|
|
57
|
+
mkdir -p $(BUILD)
|
|
58
|
+
|
|
59
|
+
# Experiments: each experiment .d becomes its own .so
|
|
60
|
+
$(BUILD)/exp_%.so: experiments/exp_%.d $(DC_SRCS) | $(BUILD)
|
|
61
|
+
$(GDC) -shared -o $@ $(DFLAGS) $(DC_SRCS) $< \
|
|
62
|
+
-L$(LIBRUBY_DIR) $(LIBRUBYARG) -lm
|
|
63
|
+
|
|
64
|
+
exp: $(addprefix $(BUILD)/,$(EXP_SRCS:=.so))
|
|
65
|
+
|
|
66
|
+
exp-test: exp
|
|
67
|
+
$(RUBY) experiments/test_exp_wrap.rb
|
|
68
|
+
$(RUBY) experiments/test_exp_cross.rb
|
|
69
|
+
$(RUBY) experiments/test_exp_perf.rb
|
|
70
|
+
$(RUBY) experiments/test_exp_http.rb
|
|
71
|
+
$(RUBY) experiments/test_exp_gcstorm.rb
|
|
72
|
+
$(RUBY) experiments/test_exp_exc.rb
|
|
73
|
+
$(RUBY) experiments/test_exp_compact.rb
|
|
74
|
+
$(RUBY) experiments/test_exp_convert.rb
|
|
75
|
+
$(RUBY) experiments/test_exp_defargs.rb
|
|
76
|
+
$(RUBY) experiments/test_exp_objproto.rb
|
|
77
|
+
$(RUBY) experiments/test_exp_nonregression.rb
|
|
78
|
+
|
|
79
|
+
ASAN_PRELOAD := LD_PRELOAD=$(shell $(GDC) -print-file-name=libasan.so)
|
|
80
|
+
# detect_odr_violation=0: wrap/cross/perf .so's each embed a copy of the rudy
|
|
81
|
+
# library also present in dextest.so (loaded by the suite loader) — ASan would
|
|
82
|
+
# otherwise abort on the duplicate registered globals.
|
|
83
|
+
ASAN_OPTS := ASAN_OPTIONS=detect_leaks=0:halt_on_error=1:allocator_may_return_null=1:detect_odr_violation=0
|
|
84
|
+
|
|
85
|
+
# ASan subset of the experiment suites (NOTES.md #23/#24). Builds lib +
|
|
86
|
+
# dextest.so + the rt_init-free exp .so's into .build.asan, then runs the
|
|
87
|
+
# corresponding test files under ruby with LD_PRELOAD=libasan.so.
|
|
88
|
+
# http is built but NOT run (NOTES.md #24: munmap crash, under investigation).
|
|
89
|
+
exp-asan-test:
|
|
90
|
+
$(MAKE) ASAN=1 --no-print-directory $(addprefix .build.asan/,$(ASAN_EXP_SRCS:=.so)) .build.asan/dextest.so
|
|
91
|
+
$(ASAN_PRELOAD) $(ASAN_OPTS) RUDY_BUILD_DIR=.build.asan $(RUBY) experiments/test_exp_wrap.rb
|
|
92
|
+
$(ASAN_PRELOAD) $(ASAN_OPTS) RUDY_BUILD_DIR=.build.asan $(RUBY) experiments/test_exp_cross.rb
|
|
93
|
+
$(ASAN_PRELOAD) $(ASAN_OPTS) RUDY_BUILD_DIR=.build.asan $(RUBY) experiments/test_exp_perf.rb
|
|
94
|
+
$(ASAN_PRELOAD) $(ASAN_OPTS) RUDY_BUILD_DIR=.build.asan $(RUBY) experiments/test_exp_exc.rb
|
|
95
|
+
$(ASAN_PRELOAD) $(ASAN_OPTS) RUDY_BUILD_DIR=.build.asan $(RUBY) experiments/test_exp_defargs.rb
|
|
96
|
+
|
|
97
|
+
exp-asan: exp-asan-test
|
|
98
|
+
|
|
99
|
+
# Memory-safety torture runs (backlog item 7). Local: ASan subset above;
|
|
100
|
+
# valgrind not installed locally (CI-only). Excluded locally: http (NOTES.md
|
|
101
|
+
# #24 munmap crash, under investigation); gcstorm/compact/convert/objproto
|
|
102
|
+
# (NOTES.md #23 rt_init SEGV).
|
|
103
|
+
memcheck: exp-asan-test
|
|
104
|
+
@echo "memcheck: ASan subset complete (see experiments/NOTES.md #23/#24)"
|
|
105
|
+
|
|
106
|
+
# Ruby-side acceptance suite (dexter revival + missing-API bindings)
|
|
107
|
+
test: all
|
|
108
|
+
$(RUBY) test/run.rb
|
|
109
|
+
$(RUBY) test/exitest.rb
|
|
110
|
+
$(RUBY) test/typedtest.rb
|
|
111
|
+
|
|
112
|
+
check: test
|
|
113
|
+
|
|
114
|
+
# --- the rudy4 gem ----------------------------------------------------------
|
|
115
|
+
#
|
|
116
|
+
# vendor/rudy/ is a copy of src/rudy/ shipped inside the gem; `vendor-check`
|
|
117
|
+
# fails if the two have drifted (CI runs it), `gem-test` covers the CLI, and
|
|
118
|
+
# `gem-test-full` also scaffolds + compiles a project with GDC.
|
|
119
|
+
$(BUILD)/vendor-synced: $(DC_SRCS)
|
|
120
|
+
@bash scripts/vendor_sync.sh
|
|
121
|
+
|
|
122
|
+
vendor: $(BUILD)/vendor-synced
|
|
123
|
+
|
|
124
|
+
vendor-check:
|
|
125
|
+
@bash scripts/vendor_check.sh
|
|
126
|
+
|
|
127
|
+
gem-test:
|
|
128
|
+
$(RUBY) -Ilib test/rudy4_test.rb
|
|
129
|
+
|
|
130
|
+
gem-test-full:
|
|
131
|
+
RUDY4_FULL=1 $(RUBY) -Ilib test/rudy4_test.rb
|
|
132
|
+
|
|
133
|
+
gem-build: vendor-check
|
|
134
|
+
gem build rudy4.gemspec
|
|
135
|
+
|
|
136
|
+
clean:
|
|
137
|
+
rm -rf .build .build.asan
|
data/README.md
ADDED
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
# rudy4
|
|
2
|
+
|
|
3
|
+
**Write Ruby native extensions in D.** A from-scratch rewrite of
|
|
4
|
+
[rudy](https://github.com/tomash/rudy) (2010), targeting **Ruby 4.0** and
|
|
5
|
+
**GDC 14 (D front-end 2.10x)**.
|
|
6
|
+
|
|
7
|
+
The 2010 original bound Ruby 1.8's C API via a bcd.gen-generated `ruby.d`,
|
|
8
|
+
required a separate `meta/` template-metaprogramming library, and built with
|
|
9
|
+
the long-dead `dsss` tool. This repo keeps the one idea worth keeping —
|
|
10
|
+
`def!` — and drops everything else.
|
|
11
|
+
|
|
12
|
+
## The idea: `def!`
|
|
13
|
+
|
|
14
|
+
Register a D function as a Ruby method with automatic argument conversion:
|
|
15
|
+
|
|
16
|
+
```d
|
|
17
|
+
module myext;
|
|
18
|
+
|
|
19
|
+
import rudy.defs;
|
|
20
|
+
|
|
21
|
+
long add_five(long n) { return n + 5; }
|
|
22
|
+
|
|
23
|
+
pragma(mangle, "Init_myext")
|
|
24
|
+
extern(C) void Init_myext()
|
|
25
|
+
{
|
|
26
|
+
def!(add_five); // global function (on Object)
|
|
27
|
+
def!("MyClass", add_five); // instance method of MyClass
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
From Ruby:
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
require "myext"
|
|
35
|
+
add_five(37) # => 42
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Compile the extension into a `myext.so` Ruby can `require` (here `myext.d`
|
|
39
|
+
lives next to a `src/` tree with the library):
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
gdc -shared -o myext.so \
|
|
43
|
+
-fPIC -Isrc \
|
|
44
|
+
$(ruby -e 'print RbConfig::CONFIG["rubyhdrdir"]') \
|
|
45
|
+
$(ruby -e 'print RbConfig::CONFIG["rubyarchhdrdir"]') \
|
|
46
|
+
src/rudy/*.d src/rudy/bind/*.d myext.d \
|
|
47
|
+
$(ruby -e 'print RbConfig::CONFIG["LIBRUBYARG_SHARED"]') \
|
|
48
|
+
-Wl,-rpath,$(ruby -e 'print RbConfig::CONFIG["libdir"]') -lm
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
or just `make` with this repo's Makefile, which does the same (header dirs,
|
|
52
|
+
`libruby` and rpath are pulled from `RbConfig`), builds `dextest.so` from
|
|
53
|
+
`src/dextest.d`, and `make test` runs the acceptance suite.
|
|
54
|
+
|
|
55
|
+
Arguments arrive as raw `VALUE`s and are converted to the declared D
|
|
56
|
+
parameter types (`long`, `double`, `string`, `bool`, ...); the return
|
|
57
|
+
value is converted back. No manual `rb_num2long` boilerplate — the
|
|
58
|
+
trampoline does it.
|
|
59
|
+
|
|
60
|
+
Supported D parameter types: `bool`, `int`/`uint`/`long`/`ulong` (`C_long`
|
|
61
|
+
aliases follow the platform), `double`, `string` (copied via `idup`),
|
|
62
|
+
static and dynamic arrays, and hashes (AAs). Return types: all of the
|
|
63
|
+
above plus `cdouble`/`creal` (mapped to Ruby `Complex` via `rb_funcallv`).
|
|
64
|
+
To receive a raw, unconverted `VALUE`, declare the parameter (or return
|
|
65
|
+
type) as `Raw!VALUE` — passthrough is opt-in since `VALUE` is an `ulong`
|
|
66
|
+
alias and a plain `ulong` param means "decode me".
|
|
67
|
+
|
|
68
|
+
Conversion semantics worth knowing (pinned by `exp_convert`):
|
|
69
|
+
- `bool` follows **Ruby truthiness** (`nil`/`false` → `false`, everything
|
|
70
|
+
else — including `0` and `""` — → `true`), matching how Ruby itself
|
|
71
|
+
sees the argument.
|
|
72
|
+
- `string` decodes the full byte content (`String#bytesize` bytes), not
|
|
73
|
+
the character count — multibyte and binary strings survive intact.
|
|
74
|
+
- `int`/`uint` decode through the range-checked converters: out-of-range
|
|
75
|
+
values raise `RangeError` instead of silently wrapping; unsigned D
|
|
76
|
+
types also reject negative inputs.
|
|
77
|
+
|
|
78
|
+
### D gotchas
|
|
79
|
+
|
|
80
|
+
The torture experiments found **twenty-four** D-specific traps; the full catalog
|
|
81
|
+
with fixes lives in experiments/NOTES.md. The ones that will bite you on
|
|
82
|
+
day one:
|
|
83
|
+
|
|
84
|
+
- **Uninitialized `double x;` is NaN** — not 0.0 (D's `float.init` is NaN).
|
|
85
|
+
Always write `double x = 0.0;` for accumulators; a NaN accumulator
|
|
86
|
+
silently poisons every sum that touches it. This bit three separate
|
|
87
|
+
times in the torture tests.
|
|
88
|
+
- **A .so extension dlopen'd by Ruby never runs D's `rt_init()`** — the D
|
|
89
|
+
GC lazy-initializes so allocation appears to work, but the first
|
|
90
|
+
`GC.collect()` or `Thread.sleep` from D hard-aborts. Call
|
|
91
|
+
`import core.runtime : rt_init; rt_init();` at the top of `Init_*`.
|
|
92
|
+
- **Module variables are thread-local by default** — Ruby threads are OS
|
|
93
|
+
pthreads, so shared state needs `__gshared`.
|
|
94
|
+
- **D exceptions must never escape an extern(C) frame** (or a
|
|
95
|
+
no-GVL callback) — catch in D, raise Ruby-side after re-acquiring the GVL.
|
|
96
|
+
- **Uninitialized integers and pointers are fine (0/null).**
|
|
97
|
+
- **gdc-14 + `-fsanitize=address` crashes on any explicit `rt_init()`**
|
|
98
|
+
(ModuleInfo NULL SEGV, finding #23) — ASan coverage is limited to the
|
|
99
|
+
rt_init-free experiments, which pass clean (`make memcheck`, finding #24).
|
|
100
|
+
|
|
101
|
+
## Wrapping D structs: `RudyTypedData`
|
|
102
|
+
|
|
103
|
+
`def!` converts *arguments*, but sometimes you want the D struct itself to
|
|
104
|
+
*be* a Ruby object (stateful objects, not just pure functions). The old
|
|
105
|
+
`Data_Wrap_Struct` path needs `rb_data_object_get`, which is an inline C
|
|
106
|
+
macro — uncallable from D. The TypedData API avoids that entirely:
|
|
107
|
+
|
|
108
|
+
```d
|
|
109
|
+
import rudy.typeddata;
|
|
110
|
+
|
|
111
|
+
struct Matrix { size_t rows, cols; double* data; }
|
|
112
|
+
|
|
113
|
+
mixin RudyTypedData!(Matrix, "matrix");
|
|
114
|
+
// generates: matrix_type (rb_data_type_t descriptor),
|
|
115
|
+
// matrix_wrap(VALUE klass, Matrix*),
|
|
116
|
+
// matrix_get(VALUE obj) // raises TypeError on wrong type
|
|
117
|
+
|
|
118
|
+
extern(C) VALUE matrix_alloc(VALUE klass)
|
|
119
|
+
{
|
|
120
|
+
import core.stdc.stdlib : calloc;
|
|
121
|
+
return matrix_wrap(klass, cast(Matrix*) calloc(1, Matrix.sizeof));
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
The mixin generates the descriptor (dfree defaults to `free()` for
|
|
126
|
+
malloc'd structs; pass `&rudy_typed_nofree` for D-GC-owned memory, or any
|
|
127
|
+
custom `extern(C) void function(void*)` for cleanup). `matrix_get` uses
|
|
128
|
+
`rb_check_typeddata`, so unwrapping a non-Matrix raises `TypeError` — no
|
|
129
|
+
D-side registry needed. Full example (alloc func, `initialize`, `[]`/
|
|
130
|
+
`[]=`, `det`, `to_a`, 100k-object GC stress): `src/dtypetest.d`, suite:
|
|
131
|
+
`test/typedtest.rb` (9 assertions).
|
|
132
|
+
|
|
133
|
+
### Optional arguments via D default values
|
|
134
|
+
|
|
135
|
+
A `def!`'d function whose parameters declare D default values is
|
|
136
|
+
registered with arity `-1` and gets a variadic (argc/argv) trampoline:
|
|
137
|
+
missing arguments are substituted with their declared defaults, so from
|
|
138
|
+
Ruby the trailing parameters behave like optional arguments.
|
|
139
|
+
|
|
140
|
+
```d
|
|
141
|
+
long var_sum(long a, long b = 2, long c = 30) { return a + b + c; }
|
|
142
|
+
def!("MyModule", var_sum);
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
```ruby
|
|
146
|
+
MyModule.var_sum(1) # => 33 (b, c defaulted)
|
|
147
|
+
MyModule.var_sum(1, 2) # => 33 (c defaulted)
|
|
148
|
+
MyModule.var_sum(1, 2, 0) # => 3
|
|
149
|
+
MyModule.var_sum # ArgumentError (a is required)
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Raw `VALUE` parameters (and `VALUE` returns via `to_ruby_value`) pass
|
|
153
|
+
through untouched, so D functions can mix optional and raw-VALUE args.
|
|
154
|
+
|
|
155
|
+
### Blocks, `*rest`, `**kwargs`, `Self`, and visibility
|
|
156
|
+
|
|
157
|
+
Special parameters are filled by the trampoline itself, never decoded
|
|
158
|
+
from argv (at most one of `Rest`/`KwArgs`/`RubyBlock` per function;
|
|
159
|
+
`RubyBlock` must be the last parameter; declaring one switches the
|
|
160
|
+
registration to the uniform argc/argv trampoline):
|
|
161
|
+
|
|
162
|
+
```d
|
|
163
|
+
import rudy.args;
|
|
164
|
+
import rudy.convert;
|
|
165
|
+
|
|
166
|
+
long with_block(long n, RubyBlock blk)
|
|
167
|
+
{
|
|
168
|
+
if (!blk.given) return n;
|
|
169
|
+
return blk.call(n); // yields n, returns block result
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
long rest_sum(long first, Rest rest) // var_sum(1, 2, 3) -> rest = [2, 3]
|
|
173
|
+
{
|
|
174
|
+
long total = first;
|
|
175
|
+
foreach (v; rest) total += d_type!long(v);
|
|
176
|
+
return total;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
string kw_pick(KwArgs kw) // kw_pick(n: 5) / kw_pick()
|
|
180
|
+
{
|
|
181
|
+
long n = kw.get!long("n", 0); // D default when the key is absent
|
|
182
|
+
import std.conv : to;
|
|
183
|
+
return "n=" ~ to!string(n) ~ (kw.given ? " (given)" : "");
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Semantics pinned by `exp_defargs` (44 assertions):
|
|
188
|
+
|
|
189
|
+
- `RubyBlock.call` with no block given raises `LocalJumpError` —
|
|
190
|
+
the same contract as `yield`. `given` tells the two cases apart;
|
|
191
|
+
calling other `def!`'d D methods from inside a block body works
|
|
192
|
+
(reentrancy).
|
|
193
|
+
- A `Rest` of zero args is an **empty array**, not `nil` — matching
|
|
194
|
+
Ruby's own `*rest` semantics (`length == 0`, `empty` agrees).
|
|
195
|
+
- KwArgs keys arrive as Symbols; `kw.to!(V, string)` normalizes them
|
|
196
|
+
via `to_s`. Decode values into concrete D types
|
|
197
|
+
(`kw.to!(long, string)`); to walk values untouched use
|
|
198
|
+
`kw.to!(Raw!VALUE, string)` — a plain `VALUE`/`ulong` V would run a
|
|
199
|
+
numeric decode over every value and raise on non-numbers.
|
|
200
|
+
- Visibility and singletons: `def_private!`/`def_protected!` define
|
|
201
|
+
instance-level methods (probe through `Object.new.extend(Mod)` or
|
|
202
|
+
`send`), `def_singleton!` a class-level method, `def_singleton_on!`
|
|
203
|
+
attaches to one specific object (`"Mod::CONST"` paths walk the scope
|
|
204
|
+
chain). `Self` receives the actual receiver.
|
|
205
|
+
- Returning a Ruby object built in D requires a `Raw!VALUE` return
|
|
206
|
+
(finding #17): a plain `VALUE` return is converted as a machine word.
|
|
207
|
+
|
|
208
|
+
## Beyond def! — direct C-API bindings
|
|
209
|
+
|
|
210
|
+
`src/rudy/bind/value.d` also binds the pieces you need to write
|
|
211
|
+
extension methods by hand (extern(C) bodies registered directly):
|
|
212
|
+
|
|
213
|
+
- **Blocks/iteration**: `rb_yield`, `rb_yield_values2`, `rb_yield_splat`,
|
|
214
|
+
`rb_block_call` (C-implemented blocks!), `rb_block_given_p`, `rb_each`
|
|
215
|
+
- **Structured exceptions**: `rb_rescue` (single StandardError family),
|
|
216
|
+
`rb_ensure`, `rb_protect`, `rb_exc_raise` (re-raise the *same* exception
|
|
217
|
+
VALUE), `rb_errinfo`/`rb_set_errinfo`
|
|
218
|
+
- **Per-object methods**: `rb_define_singleton_method`
|
|
219
|
+
- **GVL**: `rb_thread_call_without_gvl` / `_with_gvl` (pure-D work can
|
|
220
|
+
release the GVL so other Ruby threads run), `RUBY_UBF_IO`
|
|
221
|
+
- **Arg scanning**: `rb_scan_args` (argc/argv style)
|
|
222
|
+
|
|
223
|
+
`src/dexitest.d` + `test/exitest.rb` exercise all of these (31
|
|
224
|
+
assertions) alongside the fixed-arity `def!` suite; the exception and
|
|
225
|
+
GVL bindings are additionally tortured by `experiments/exp_exc` and
|
|
226
|
+
`experiments/exp_http` (see experiments/NOTES.md).
|
|
227
|
+
|
|
228
|
+
## Layout
|
|
229
|
+
|
|
230
|
+
```
|
|
231
|
+
lib/rudy4/ the rudy4 gem: CLI, generator, doctor, templates
|
|
232
|
+
bin/rudy4 the rudy4 command
|
|
233
|
+
vendor/rudy/ vendored copy of src/rudy/ (shipped in the gem)
|
|
234
|
+
rudy4.gemspec the gem itself
|
|
235
|
+
scripts/vendor_sync.sh src/rudy/ -> vendor/rudy/ (vendor_check.sh guards drift)
|
|
236
|
+
src/rudy/bind/value.d hand-written binding to the Ruby 4.0 C API
|
|
237
|
+
src/rudy/convert.d to_ruby_value!(T) / d_type!(T) conversions
|
|
238
|
+
src/rudy/defs.d def! templates + extern(C) trampolines
|
|
239
|
+
src/rudy/typeddata.d RudyTypedData mixin: wrap D structs as Ruby objects
|
|
240
|
+
src/rudy/objproto.d RudyObjectProtocol mixin: dup/clone/==/hash/inspect/freeze
|
|
241
|
+
src/dextest.d acceptance-test extension (old "dexter" revived)
|
|
242
|
+
src/dexitest.d missing-API bindings acceptance extension
|
|
243
|
+
src/dtypetest.d typed-data acceptance extension
|
|
244
|
+
experiments/ eleven torture experiments (see experiments/NOTES.md)
|
|
245
|
+
examples/hello_gem/ gem-shaped extension example (gemspec + ext/ + lib/)
|
|
246
|
+
test/rudy4_test.rb the gem's own suite (scaffold / doctor / vendor)
|
|
247
|
+
test/run.rb Ruby-side acceptance suite (22 assertions)
|
|
248
|
+
test/exitest.rb bindings acceptance suite (31 assertions)
|
|
249
|
+
test/typedtest.rb typed-data acceptance suite (9 assertions)
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
`src/rudy/bind/value.d` is deliberately **not** generated: the modern Ruby
|
|
253
|
+
API is mostly opaque `VALUE` + function calls, so ~200 lines of hand-picked
|
|
254
|
+
`extern(C)` declarations replace the old 2,400-line bcd.gen blob. Struct
|
|
255
|
+
accesses (`RSTRING(str).ptr`) are gone — everything goes through exported
|
|
256
|
+
functions (`rb_string_value_ptr`, `rb_float_value`, `rb_ary_entry`, ...).
|
|
257
|
+
|
|
258
|
+
## Getting started: the `rudy4` gem
|
|
259
|
+
|
|
260
|
+
The fastest way in is the `rudy4` gem. It scaffolds a self-contained
|
|
261
|
+
extension gem — the rudy4 D library is copied into it, so the project builds
|
|
262
|
+
with nothing but GDC and your Ruby headers, no checkout and no `RUDY4_ROOT`:
|
|
263
|
+
|
|
264
|
+
```sh
|
|
265
|
+
gem install rudy4
|
|
266
|
+
rudy4 doctor # checks Ruby + headers + GDC, then compiles a probe
|
|
267
|
+
rudy4 init fast_math # writes a small extension gem
|
|
268
|
+
cd fast_math
|
|
269
|
+
rake compile # GDC builds ext/fast_math/fast_math.d
|
|
270
|
+
rake test # 7 assertions against the compiled extension
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
Now edit `ext/fast_math/fast_math.d` and repeat `rake compile && rake test`.
|
|
274
|
+
The generated project is a normal gem: add it to another app as a path gem,
|
|
275
|
+
or `gem build` and publish it — RubyGems will compile the D source at install
|
|
276
|
+
time.
|
|
277
|
+
|
|
278
|
+
`rudy4 doctor` is the thing to run when a build fails: it verifies the Ruby
|
|
279
|
+
toolchain, reports the GDC version, and then *proves* the setup by scaffolding
|
|
280
|
+
and compiling a throwaway extension, so a failure names the missing piece
|
|
281
|
+
instead of dumping compiler output.
|
|
282
|
+
|
|
283
|
+
## Requirements
|
|
284
|
+
|
|
285
|
+
- Ruby 4.0 (any install with `libruby` and headers)
|
|
286
|
+
- GDC ≥ 13 (tested: gdc-14 14.2.0, D front-end 2.10x)
|
|
287
|
+
- GNU make
|
|
288
|
+
|
|
289
|
+
## Packaging: shipping a D-backed gem
|
|
290
|
+
|
|
291
|
+
Two worked examples, in order of usefulness:
|
|
292
|
+
|
|
293
|
+
1. **`rudy4 init NAME`** (the `rudy4` gem, above) — the recommended path. It
|
|
294
|
+
writes a project whose `ext/NAME/rudy/` is a *vendored copy* of the rudy4 D
|
|
295
|
+
library, so it builds and installs with no reference to a rudy4 checkout.
|
|
296
|
+
2. **`examples/hello_gem/`** — the in-repo reference for what that generated
|
|
297
|
+
layout can contain (wrapped D struct, object protocol, freeze-aware
|
|
298
|
+
mutators). It predates the gem and finds its sources by walking up to a
|
|
299
|
+
checkout (`RUDY4_ROOT`), so it is a development example, not the first-run
|
|
300
|
+
path:
|
|
301
|
+
|
|
302
|
+
```sh
|
|
303
|
+
cd examples/hello_gem
|
|
304
|
+
bash test/smoke.sh # build + 17 assertions
|
|
305
|
+
gem build hello_gem.gemspec
|
|
306
|
+
RUDY4_ROOT=/path/to/rudy4 gem install ./hello_gem-0.1.0.gem
|
|
307
|
+
ruby -e 'require "hello_gem"; puts add(2, 3)'
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
See `examples/hello_gem/README.md` for how the extconf.rb trick works and
|
|
311
|
+
why installing from outside the repo needs `RUDY4_ROOT`.
|
|
312
|
+
|
|
313
|
+
### The `rudy4` gem itself
|
|
314
|
+
|
|
315
|
+
`lib/rudy4/`, `bin/rudy4`, `vendor/rudy/` and `rudy4.gemspec` are the gem:
|
|
316
|
+
`init` (scaffolder), `doctor` (toolchain check + build probe), `vendor`
|
|
317
|
+
(re-copy the D library into an existing project). `vendor/rudy/` is generated
|
|
318
|
+
from `src/rudy/` by `scripts/vendor_sync.sh`; CI runs
|
|
319
|
+
`scripts/vendor_check.sh` and fails if the two drift, so `src/rudy/` stays the
|
|
320
|
+
single source of truth.
|
|
321
|
+
|
|
322
|
+
## Torture experiments (`experiments/`)
|
|
323
|
+
|
|
324
|
+
Eleven experiments probe the D↔Ruby seam far past what the acceptance
|
|
325
|
+
suites cover — GVL-released blocking I/O with a hand-written TCP server,
|
|
326
|
+
48k-object TypedData GC storms against a concurrent `GC.collect()` churn
|
|
327
|
+
thread, 50-level exception ping-pong with `rb_rescue`/`rb_ensure`/nested
|
|
328
|
+
`rb_protect` and `rb_exc_raise` identity, perf, conversion round trips
|
|
329
|
+
across every supported D type (integers at ±2^63/2^64-1, NaN/inf,
|
|
330
|
+
multibyte/binary/invalid-encoding strings, GC.stress at the seam), the
|
|
331
|
+
`def!` argument-passing machinery itself (blocks, `*rest`, `**kwargs`,
|
|
332
|
+
visibility, singletons), a full object protocol (`dup`/`clone`/`==`/
|
|
333
|
+
`hash`/`inspect`/`freeze`), and a non-regression suite for the documented
|
|
334
|
+
non-goals (Fiber switches, Ractor refusal, GVL-held interruptibility).
|
|
335
|
+
They found
|
|
336
|
+
nineteen real bugs (ABI widths, a missing `rt_init()`, thread-local-by-default module
|
|
337
|
+
state, a TypedData free-callback routing bug, ...) plus eight toolchain /
|
|
338
|
+
boundary findings (#20–#27: object-protocol semantics, the zalloc
|
|
339
|
+
size-arg bug, GDC template-arg quirks, the gdc-14 ASan/rt_init
|
|
340
|
+
limitations, and the three non-goals), all
|
|
341
|
+
documented with
|
|
342
|
+
fixes in **experiments/NOTES.md** — that file is the real catalog of
|
|
343
|
+
D-specific traps when writing Ruby extensions. Run them with `make exp`
|
|
344
|
+
then `make exp-test`.
|
|
345
|
+
|
|
346
|
+
## Known limitations (non-goals)
|
|
347
|
+
|
|
348
|
+
Three things rudy deliberately does **not** promise. Each is pinned by
|
|
349
|
+
`experiments/test_exp_nonregression.rb` so the boundary is evidence, not
|
|
350
|
+
silence (see NOTES.md findings #25–#27):
|
|
351
|
+
|
|
352
|
+
- **Fiber switching across D frames** — simple value round-trips across a
|
|
353
|
+
`Fiber.yield` inside a D frame work, but a D-GC object reachable only
|
|
354
|
+
from a fiber-stack local has no GC root (D's GC scans the thread stack,
|
|
355
|
+
not Ruby's fiber stacks).
|
|
356
|
+
- **Ractor-shareability** — calling `def!`'d methods from a non-main
|
|
357
|
+
Ractor is refused by the VM (`Ractor::UnsafeError`); D-wrapped objects
|
|
358
|
+
are not shareable. Use threads or separate processes.
|
|
359
|
+
- **Interruptibility of a long GVL-held D call** — a pure-D call holding
|
|
360
|
+
the GVL ignores Ctrl-C until it returns. Run long work under
|
|
361
|
+
`rb_thread_call_without_gvl` (with a cooperating unblock function) and
|
|
362
|
+
it becomes interruptible in milliseconds.
|
|
363
|
+
|
|
364
|
+
## Build & test
|
|
365
|
+
|
|
366
|
+
```sh
|
|
367
|
+
make # builds .build/dextest.so, .build/dexitest.so and .build/dtypetest.so
|
|
368
|
+
make test # runs test/run.rb + test/exitest.rb + test/typedtest.rb (62 assertions)
|
|
369
|
+
make exp # builds the eleven experiment extensions (experiments/)
|
|
370
|
+
make exp-test # runs all eleven torture suites (452 assertions, see experiments/NOTES.md)
|
|
371
|
+
make memcheck # ASan subset: builds .build.asan and runs the rt_init-free suites
|
|
372
|
+
|
|
373
|
+
make gem-test # the rudy4 gem's own suite (scaffold / doctor / vendor, 136 assertions)
|
|
374
|
+
make gem-test-full # + scaffolds a project and compiles it with GDC, end to end
|
|
375
|
+
make vendor-check # fails if vendor/rudy/ has drifted from src/rudy/
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
The Makefile finds Ruby via `ruby -e` hdrdir/libdir queries (works with
|
|
379
|
+
asdf, rbenv, system Ruby). Point `RUBY` at a specific binary if needed:
|
|
380
|
+
|
|
381
|
+
```sh
|
|
382
|
+
make test RUBY=~/.asdf/installs/ruby/4.0.7/bin/ruby
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
## Differences from 2010 rudy
|
|
386
|
+
|
|
387
|
+
| 2010 rudy | rudy4 |
|
|
388
|
+
|---|---|
|
|
389
|
+
| Ruby 1.8 | Ruby 4.0 |
|
|
390
|
+
| D1 | D2 (GDC 14) |
|
|
391
|
+
| `VALUE` = `uint` | `VALUE` = `ulong` (pointer-sized) |
|
|
392
|
+
| bcd.gen'd 2,415-line `ruby.d` | ~220-line hand-written binding |
|
|
393
|
+
| `RSTRING(s).ptr` struct access | function API only |
|
|
394
|
+
| `meta/` TMP library (~870 lines) | `std.traits` (`Parameters!`, `__traits`) |
|
|
395
|
+
| `dsss` build | plain Makefile |
|
|
396
|
+
| raw VALUE passthrough in def! | automatic D↔Ruby conversion |
|
|
397
|
+
|
|
398
|
+
## License
|
|
399
|
+
|
|
400
|
+
MIT — see [LICENSE](LICENSE) (as the original rudy).
|