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