carray-jit 0.1.6 → 0.1.7
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 +4 -4
- data/CHANGELOG.md +22 -0
- data/README.md +1 -1
- data/carray-jit.gemspec +4 -2
- data/docs/02_KernelShapes.md +2 -2
- data/docs/03_SupportedFeatures.md +1 -1
- data/docs/04_Compiling.md +5 -5
- data/lib/carray/jit/analyzer.rb +1 -1
- data/lib/carray/jit/c_function.rb +133 -139
- data/lib/carray/jit/c_generator.rb +19 -10
- data/lib/carray/jit/compiler.rb +44 -26
- data/lib/carray/jit/expression.rb +68 -28
- data/lib/carray/jit/kernel.rb +23 -47
- data/lib/carray/jit/native.rb +149 -0
- data/lib/carray/jit/sweep.rb +14 -64
- data/lib/carray/jit/version.rb +1 -1
- data/lib/carray/jit.rb +96 -19
- metadata +7 -6
data/lib/carray/jit/compiler.rb
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
require "digest"
|
|
2
2
|
require "fileutils"
|
|
3
3
|
require "rbconfig"
|
|
4
|
+
require "securerandom"
|
|
4
5
|
require "tmpdir"
|
|
5
|
-
require "
|
|
6
|
+
require "carray/jit/native"
|
|
6
7
|
require "monitor"
|
|
7
8
|
|
|
8
9
|
class CArray
|
|
@@ -44,7 +45,7 @@ class CArray
|
|
|
44
45
|
|
|
45
46
|
# Every generated object exports the same fixed names -- carray_jit_kernel,
|
|
46
47
|
# carray_jit_slab, the border and error helpers -- because each one is a
|
|
47
|
-
# module of its own.
|
|
48
|
+
# module of its own. Native opens each with RTLD_GLOBAL, so on ELF those
|
|
48
49
|
# names all land in one flat namespace and an intra-module call resolves
|
|
49
50
|
# to the first definition loaded: a later kernel's carray_jit_slab calls
|
|
50
51
|
# an earlier kernel's carray_jit_kernel, which takes a different number
|
|
@@ -83,10 +84,11 @@ class CArray
|
|
|
83
84
|
LINK = (RbConfig::CONFIG["host_os"] =~ /darwin/ ?
|
|
84
85
|
["-undefined", "dynamic_lookup"] : []).freeze
|
|
85
86
|
|
|
86
|
-
# Kernels retained on disk. Each costs about
|
|
87
|
-
#
|
|
88
|
-
#
|
|
89
|
-
|
|
87
|
+
# Kernels retained on disk. Each costs about 30 KB, object and source,
|
|
88
|
+
# so the default is roughly 60 MB. Rebuilding is what a bound too low
|
|
89
|
+
# costs -- on macOS a new object's first load is checked by the system,
|
|
90
|
+
# around 200 ms each -- and this gem's own suite wants some 1300.
|
|
91
|
+
DEFAULT_ENTRY_LIMIT = 2000
|
|
90
92
|
|
|
91
93
|
class << self
|
|
92
94
|
|
|
@@ -223,7 +225,7 @@ class CArray
|
|
|
223
225
|
removed
|
|
224
226
|
end
|
|
225
227
|
|
|
226
|
-
# Returns [
|
|
228
|
+
# Returns [Native library, whether it was compiled rather than reused].
|
|
227
229
|
#
|
|
228
230
|
# `header` says where the kernel was written. It is written into the
|
|
229
231
|
# .c file but kept out of the digest: two call sites that produce the
|
|
@@ -231,14 +233,16 @@ class CArray
|
|
|
231
233
|
# kernel does not throw its object away. The file then names the
|
|
232
234
|
# first site that compiled it, which is a place the code was written
|
|
233
235
|
# rather than the only one.
|
|
234
|
-
# One compile at a time in a process
|
|
235
|
-
#
|
|
236
|
-
#
|
|
237
|
-
#
|
|
238
|
-
#
|
|
239
|
-
#
|
|
240
|
-
#
|
|
241
|
-
#
|
|
236
|
+
# One compile at a time in a process, which spares a second thread the
|
|
237
|
+
# build: it takes the lock after the first has published, and finds
|
|
238
|
+
# the object where a later run would. It is not what keeps two builds
|
|
239
|
+
# apart -- each stages under a name of its own (#staging_name), so
|
|
240
|
+
# builds that meet anyway, as Ractors would, each publish a whole
|
|
241
|
+
# object. Until the names were that, two threads staged to one path
|
|
242
|
+
# and whichever renamed first left the other renaming a path that was
|
|
243
|
+
# gone -- a bare Errno::ENOENT out of three threads in four.
|
|
244
|
+
# Reentrant, so that a build reached from inside another -- which
|
|
245
|
+
# none is today -- would not stop here.
|
|
242
246
|
LOCK = Monitor.new
|
|
243
247
|
|
|
244
248
|
def build (source, function_name, header: nil, flags: FLAGS)
|
|
@@ -266,12 +270,17 @@ class CArray
|
|
|
266
270
|
end
|
|
267
271
|
|
|
268
272
|
source_path = File.join(directory, "#{key}.c")
|
|
269
|
-
|
|
270
|
-
#
|
|
271
|
-
#
|
|
272
|
-
|
|
273
|
+
# Both written to a path of this build's own and renamed into
|
|
274
|
+
# place, so that nothing -- another build, another process -- ever
|
|
275
|
+
# reads half of either. A rename onto a path another build has
|
|
276
|
+
# just published replaces the same bytes.
|
|
277
|
+
staging = staging_name
|
|
278
|
+
staged_source = "#{source_path}.#{staging}"
|
|
279
|
+
staging_path = "#{object_path}.#{staging}"
|
|
273
280
|
handle =
|
|
274
281
|
begin
|
|
282
|
+
File.write(staged_source, header.to_s + source)
|
|
283
|
+
File.rename(staged_source, source_path)
|
|
275
284
|
compile(source_path, staging_path, flags)
|
|
276
285
|
# Opened before it is published, under the name only this
|
|
277
286
|
# process knows. Published first, the object stands in the
|
|
@@ -291,6 +300,7 @@ class CArray
|
|
|
291
300
|
# that cannot compile. The staging file would be swept in five
|
|
292
301
|
# minutes; it goes now, beside the source it came from.
|
|
293
302
|
remove_entry(object_path)
|
|
303
|
+
unlink(staged_source)
|
|
294
304
|
unlink(staging_path)
|
|
295
305
|
raise
|
|
296
306
|
end
|
|
@@ -469,15 +479,23 @@ class CArray
|
|
|
469
479
|
end
|
|
470
480
|
end
|
|
471
481
|
|
|
472
|
-
#
|
|
473
|
-
#
|
|
474
|
-
#
|
|
475
|
-
|
|
482
|
+
# The suffix one build stages its files under: the process, and a
|
|
483
|
+
# random part for the build within it. Not a counter, which would be
|
|
484
|
+
# state shared between the threads and Ractors that build at once.
|
|
485
|
+
def staging_name
|
|
486
|
+
"#{Process.pid}.#{SecureRandom.hex(4)}"
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
# A process killed mid-compile leaves `<key>.bundle.<staging>` behind,
|
|
490
|
+
# and `<key>.c.<staging>` if it was killed sooner. Those match neither
|
|
491
|
+
# the entry glob nor the prune glob, so without this they would sit in
|
|
492
|
+
# the cache for good. Only swept on a miss, to keep it off the reuse
|
|
493
|
+
# path.
|
|
476
494
|
STAGING_MAXIMUM_AGE = 300
|
|
477
495
|
|
|
478
496
|
def sweep_staging (directory)
|
|
479
497
|
now = Time.now
|
|
480
|
-
Dir[File.join(directory, "
|
|
498
|
+
Dir[File.join(directory, "*{#{shared_object_suffix},.c}.*")].each do |path|
|
|
481
499
|
next unless File.file?(path)
|
|
482
500
|
age = now - File.mtime(path) rescue 0
|
|
483
501
|
next if age < STAGING_MAXIMUM_AGE
|
|
@@ -572,8 +590,8 @@ class CArray
|
|
|
572
590
|
# rebuilt. A freshly compiled object that will not load is a real
|
|
573
591
|
# failure and is raised by #load_new_object.
|
|
574
592
|
def load_shared_object (path)
|
|
575
|
-
|
|
576
|
-
rescue
|
|
593
|
+
Native.open_library(path, global: true)
|
|
594
|
+
rescue Native::LinkError
|
|
577
595
|
nil
|
|
578
596
|
end
|
|
579
597
|
|
|
@@ -83,9 +83,11 @@ class CArray
|
|
|
83
83
|
# Held here so that the buffers outlive the call.
|
|
84
84
|
dim = out.dim.pack("q*")
|
|
85
85
|
steps = strided.flat_map { |slot| slot_strides(bases, slot) }.pack("q*")
|
|
86
|
+
constants = packed_constants(plan) or return false
|
|
86
87
|
args = [out.elements]
|
|
87
|
-
args <<
|
|
88
|
-
args <<
|
|
88
|
+
args << dim if walks_axes?(plan, strided)
|
|
89
|
+
args << steps unless strided.empty?
|
|
90
|
+
args << constants unless constants.empty?
|
|
89
91
|
kernel.call(*args, *pointers(plan, bases))
|
|
90
92
|
end
|
|
91
93
|
true
|
|
@@ -127,10 +129,9 @@ class CArray
|
|
|
127
129
|
@strided = strided
|
|
128
130
|
source = source_for(plan, aliased) or return nil
|
|
129
131
|
handle, = Compiler.build(source, "carray_jit_expression", flags: FLAGS)
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
Fiddle::TYPE_VOID)
|
|
132
|
+
Native.function(Native.address(handle, "carray_jit_expression"),
|
|
133
|
+
[:long_long] + [:pointer] * (1 + arity(plan, strided)),
|
|
134
|
+
:void, release_gvl: true)
|
|
134
135
|
rescue CompilationError
|
|
135
136
|
nil
|
|
136
137
|
end
|
|
@@ -138,7 +139,63 @@ class CArray
|
|
|
138
139
|
def arity (plan, strided)
|
|
139
140
|
plan.leaves.size + plan.leaves.count { |array| array.has_mask? } +
|
|
140
141
|
(plan.masked ? 1 : 0) + (walks_axes?(plan, strided) ? 1 : 0) +
|
|
141
|
-
(strided.empty? ? 0 : 1)
|
|
142
|
+
(strided.empty? ? 0 : 1) + (constant_nodes(plan).empty? ? 0 : 1)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# The values the kernel does not have written into it: each constant,
|
|
146
|
+
# and the fill of each shifted read that fills. They arrive at the call
|
|
147
|
+
# instead. Written into the C as literals, they were the compiler's to
|
|
148
|
+
# fold, and it folded what CArray computes as arithmetic: `x * -1.0`
|
|
149
|
+
# became a negation, which flips the sign of a NaN that the multiply
|
|
150
|
+
# in CArray's own kernel leaves as it is. A value the compiler cannot
|
|
151
|
+
# see is one it computes with as written.
|
|
152
|
+
def constant_nodes (plan)
|
|
153
|
+
plan.nodes.each_with_index.filter_map { |node, i|
|
|
154
|
+
case node
|
|
155
|
+
when CArray::Fusion::Const then [i, node]
|
|
156
|
+
when CArray::Fusion::Shifted
|
|
157
|
+
[i, CArray::Fusion::Const.new(node.fill, node.data_type)] if node.bounds.uniq == [:fill]
|
|
158
|
+
end
|
|
159
|
+
}
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# One slot of CONSTANT_SLOT bytes per value, in the order of
|
|
163
|
+
# #constant_nodes, each read back with memcpy so that no slot need be
|
|
164
|
+
# aligned. Nil for a value that has no bytes in its type -- the plan is
|
|
165
|
+
# then declined.
|
|
166
|
+
def packed_constants (plan)
|
|
167
|
+
constant_nodes(plan).map { |_, node| constant_bytes(node) or return nil }.join
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
CONSTANT_SLOT = 8
|
|
171
|
+
|
|
172
|
+
PACKING = {
|
|
173
|
+
float64: "d", float32: "f",
|
|
174
|
+
int8: "c", int16: "s", int32: "l", int64: "q",
|
|
175
|
+
uint8: "C", uint16: "S", uint32: "L", uint64: "Q",
|
|
176
|
+
boolean: "C",
|
|
177
|
+
}.freeze
|
|
178
|
+
|
|
179
|
+
def constant_bytes (node)
|
|
180
|
+
directive = PACKING[node.data_type] or return nil
|
|
181
|
+
value = node.value
|
|
182
|
+
value = case node.data_type
|
|
183
|
+
when :float64, :float32 then Float(value)
|
|
184
|
+
when :boolean then truth(value) ? 1 : 0
|
|
185
|
+
else value.is_a?(Float) ? (value.finite? ? value.to_i : (return nil)) : Integer(value)
|
|
186
|
+
end
|
|
187
|
+
[value].pack(directive).ljust(CONSTANT_SLOT, "\0")
|
|
188
|
+
rescue TypeError, ArgumentError, RangeError
|
|
189
|
+
nil
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# The constants read into locals before the loop, where they stay for
|
|
193
|
+
# every cell.
|
|
194
|
+
def constant_lines (plan)
|
|
195
|
+
constant_nodes(plan).each_with_index.map { |(i, node), j|
|
|
196
|
+
type = C_TYPES.fetch(node.data_type)
|
|
197
|
+
" #{type} c#{i}; memcpy(&c#{i}, constants + #{j * CONSTANT_SLOT}, sizeof c#{i});"
|
|
198
|
+
}
|
|
142
199
|
end
|
|
143
200
|
|
|
144
201
|
# A plan that reads an operand shifted, or by strides, needs to know
|
|
@@ -256,9 +313,10 @@ class CArray
|
|
|
256
313
|
|
|
257
314
|
#{helpers}
|
|
258
315
|
void
|
|
259
|
-
carray_jit_expression (int64_t elements#{walking ? ", const int64_t *dim" : ""}#{@strided.empty? ? "" : ", const int64_t *steps"}, #{out_type} *#{restrict}out#{mask_parameter(plan, restrict)}#{parameters(plan, restrict)})
|
|
316
|
+
carray_jit_expression (int64_t elements#{walking ? ", const int64_t *dim" : ""}#{@strided.empty? ? "" : ", const int64_t *steps"}#{constant_nodes(plan).empty? ? "" : ", const unsigned char *constants"}, #{out_type} *#{restrict}out#{mask_parameter(plan, restrict)}#{parameters(plan, restrict)})
|
|
260
317
|
{
|
|
261
318
|
#{stride_lines(plan).join("\n")}
|
|
319
|
+
#{constant_lines(plan).join("\n")}
|
|
262
320
|
#{loop(plan, body + result, walking && (statements(plan, :inside) + result)).join("\n")}
|
|
263
321
|
}
|
|
264
322
|
C
|
|
@@ -361,7 +419,7 @@ class CArray
|
|
|
361
419
|
when CArray::Fusion::Shifted
|
|
362
420
|
shifted_line(plan, node, i, type)
|
|
363
421
|
when CArray::Fusion::Const
|
|
364
|
-
["#{type} v#{i} = #{
|
|
422
|
+
["#{type} v#{i} = c#{i};",
|
|
365
423
|
*(plan.masked ? ["uint8_t m#{i} = 0;"] : [])]
|
|
366
424
|
when CArray::Fusion::Op
|
|
367
425
|
statement = substitute(node, i, type) or return nil
|
|
@@ -397,8 +455,7 @@ class CArray
|
|
|
397
455
|
clamped = (0...ndim).map { |k| "c#{i}_#{k}" }
|
|
398
456
|
lines << "#{type} v#{i} = #{cell("a#{node.index}", type, flat, clamped)};"
|
|
399
457
|
if bounds.first == :fill
|
|
400
|
-
|
|
401
|
-
lines << "if ( ! s#{i}_in ) { v#{i} = #{literal(fill)}; }"
|
|
458
|
+
lines << "if ( ! s#{i}_in ) { v#{i} = c#{i}; }"
|
|
402
459
|
end
|
|
403
460
|
if plan.masked
|
|
404
461
|
inner = node.masked ? cell("k#{node.index}", "uint8_t", flat, clamped) : "0"
|
|
@@ -408,23 +465,6 @@ class CArray
|
|
|
408
465
|
lines
|
|
409
466
|
end
|
|
410
467
|
|
|
411
|
-
def literal (node)
|
|
412
|
-
case node.data_type
|
|
413
|
-
when :float64, :float32 then float_literal(node.value.to_f)
|
|
414
|
-
when :boolean then truth(node.value) ? 1 : 0
|
|
415
|
-
else node.value.to_s
|
|
416
|
-
end
|
|
417
|
-
end
|
|
418
|
-
|
|
419
|
-
# `%.17g` writes -0.0 as "-0", which C reads as the integer 0 negated,
|
|
420
|
-
# and writes NaN and the infinities as words C does not know.
|
|
421
|
-
def float_literal (value)
|
|
422
|
-
return "NAN" if value.nan?
|
|
423
|
-
return (value > 0 ? "INFINITY" : "(-INFINITY)") if value.infinite?
|
|
424
|
-
text = "%.17g" % value
|
|
425
|
-
text.match?(/[.e]/) ? text : "#{text}.0"
|
|
426
|
-
end
|
|
427
|
-
|
|
428
468
|
# A boolean cell as CArray stores it: 0 is false, as is false.
|
|
429
469
|
def truth (value)
|
|
430
470
|
value.is_a?(Numeric) ? value != 0 : !!value
|
data/lib/carray/jit/kernel.rb
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
require "
|
|
1
|
+
require "carray/jit/native"
|
|
2
2
|
require "carray/jit/sweep"
|
|
3
3
|
|
|
4
4
|
class CArray
|
|
@@ -7,8 +7,9 @@ class CArray
|
|
|
7
7
|
# A compiled kernel, bound to one set of array data types and one set of
|
|
8
8
|
# scalar types.
|
|
9
9
|
#
|
|
10
|
-
# Every kernel has the same C signature, so the
|
|
11
|
-
# fixed and the per-kernel detail travels in the six
|
|
10
|
+
# Every kernel has the same C signature, so the shape of the function
|
|
11
|
+
# Native makes is fixed and the per-kernel detail travels in the six
|
|
12
|
+
# buffers.
|
|
12
13
|
class CompiledKernel
|
|
13
14
|
|
|
14
15
|
# @!attribute [r] source
|
|
@@ -54,6 +55,12 @@ class CArray
|
|
|
54
55
|
@rank = analyzer.rank
|
|
55
56
|
@index_names = analyzer.index_names
|
|
56
57
|
@written_arrays = analyzer.written_arrays
|
|
58
|
+
# Which operands the sweep writes, in CArray's spelling: one character
|
|
59
|
+
# per operand. Decided by the body, so spelled once rather than on
|
|
60
|
+
# every call.
|
|
61
|
+
@fsync = @arrays.map { |name|
|
|
62
|
+
@written_arrays.include?(name) ? "1" : "0"
|
|
63
|
+
}.join.freeze
|
|
57
64
|
@array_ranks = analyzer.array_ranks
|
|
58
65
|
@inner_ranges = analyzer.inner_ranges
|
|
59
66
|
@unbounded_indices = analyzer.unbounded_indices
|
|
@@ -126,9 +133,8 @@ class CArray
|
|
|
126
133
|
@handle, @compiled = Compiler.build(generator.generate_slab(SLAB_NAME),
|
|
127
134
|
CGenerator::FUNCTION_NAME,
|
|
128
135
|
header: generator.provenance)
|
|
129
|
-
@function =
|
|
130
|
-
|
|
131
|
-
Fiddle::TYPE_VOID)
|
|
136
|
+
@function = Native.function(Native.address(@handle, CGenerator::FUNCTION_NAME),
|
|
137
|
+
[:pointer] * 10, :void, release_gvl: true)
|
|
132
138
|
end
|
|
133
139
|
|
|
134
140
|
# `bounds` is one [start, limit, step] per axis.
|
|
@@ -209,9 +215,6 @@ class CArray
|
|
|
209
215
|
# arguments the kernel's own loop takes are the ones a chunk arrives as.
|
|
210
216
|
def sweep (array_values, scalar_values, c_function_values = {})
|
|
211
217
|
arrays = @arrays.map { |name| array_values.fetch(name) }
|
|
212
|
-
fsync = @arrays.map { |name|
|
|
213
|
-
@written_arrays.include?(name) ? "1" : "0"
|
|
214
|
-
}.join
|
|
215
218
|
|
|
216
219
|
error = [0].pack("l")
|
|
217
220
|
reals = packed_reals(scalar_values)
|
|
@@ -233,12 +236,10 @@ class CArray
|
|
|
233
236
|
# signature has the slots.
|
|
234
237
|
masks = ("\0" * 8)
|
|
235
238
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
slots.last[0, error.bytesize]
|
|
241
|
-
}
|
|
239
|
+
# The slab writes the error slot in `error` itself.
|
|
240
|
+
Sweep.run(slab_pointer, @fsync, arrays,
|
|
241
|
+
buffer(reals), buffer(integers), buffer(functions),
|
|
242
|
+
buffer(data), masks, masks, error)
|
|
242
243
|
|
|
243
244
|
packed.each_with_index do |(array, item), index|
|
|
244
245
|
next if array.equal?(item)
|
|
@@ -332,7 +333,7 @@ class CArray
|
|
|
332
333
|
@shape_scalar_names.map { |name| scalars[name] }]
|
|
333
334
|
end
|
|
334
335
|
|
|
335
|
-
# A kernel is shared, and
|
|
336
|
+
# A kernel is shared, and its call lets go of the GVL, so two
|
|
336
337
|
# threads can be reading one plan while a third prepares another. That
|
|
337
338
|
# is sound only while nothing in a plan is written after it is published
|
|
338
339
|
# -- and a box is arrays inside an array, so freezing the top of it
|
|
@@ -456,7 +457,7 @@ class CArray
|
|
|
456
457
|
end
|
|
457
458
|
|
|
458
459
|
def slab_pointer
|
|
459
|
-
@slab_pointer ||= @handle
|
|
460
|
+
@slab_pointer ||= Native.address(@handle, SLAB_NAME)
|
|
460
461
|
end
|
|
461
462
|
|
|
462
463
|
# Resolved when the frame is first walked rather than at build time: a
|
|
@@ -464,8 +465,8 @@ class CArray
|
|
|
464
465
|
# runs its interior first.
|
|
465
466
|
def border_function
|
|
466
467
|
@border_function ||=
|
|
467
|
-
|
|
468
|
-
|
|
468
|
+
Native.function(Native.address(@handle, CGenerator::BORDER_NAME),
|
|
469
|
+
[:pointer] * 10, :void, release_gvl: true)
|
|
469
470
|
end
|
|
470
471
|
|
|
471
472
|
# What a C function may be handed as a pointer, and what has to be true
|
|
@@ -523,7 +524,7 @@ class CArray
|
|
|
523
524
|
|
|
524
525
|
def address_buffer (name, array)
|
|
525
526
|
@address_parameters.fetch(name).each do |parameter|
|
|
526
|
-
wanted = CDeclaration::DATA_TYPES.fetch(parameter.element.
|
|
527
|
+
wanted = CDeclaration::DATA_TYPES.fetch(parameter.element.abi)
|
|
527
528
|
unless array.data_type_name == wanted.to_s
|
|
528
529
|
raise Unsupported,
|
|
529
530
|
"`#{name}` is handed to a C function as `#{parameter.text}`, " \
|
|
@@ -553,37 +554,12 @@ class CArray
|
|
|
553
554
|
Access.classify(array)[:entity] ? array : array.copy
|
|
554
555
|
end
|
|
555
556
|
|
|
556
|
-
# A pointer into an empty string is not something
|
|
557
|
-
# and a kernel with no scalars has an empty buffer.
|
|
557
|
+
# A pointer into an empty string is not something Native is asked to
|
|
558
|
+
# hand over, and a kernel with no scalars has an empty buffer.
|
|
558
559
|
def buffer (packed)
|
|
559
560
|
packed.empty? ? "\0" * 8 : packed
|
|
560
561
|
end
|
|
561
562
|
|
|
562
|
-
# Copies `parts` into one block of C memory, each part 8-byte aligned,
|
|
563
|
-
# after a table of their addresses, and yields a pointer to the table
|
|
564
|
-
# and a pointer to each part; the block is freed when this returns.
|
|
565
|
-
#
|
|
566
|
-
# For a call that is handed addresses inside other memory rather than
|
|
567
|
-
# arguments: a Ruby String's bytes may be moved by GC compaction, and
|
|
568
|
-
# an address taken from one and packed elsewhere is not one Ruby knows
|
|
569
|
-
# to keep in place. Memory from malloc is never moved.
|
|
570
|
-
def in_one_block (parts)
|
|
571
|
-
table = parts.size * Fiddle::SIZEOF_VOIDP
|
|
572
|
-
offsets = parts.each_with_object([]) { |part, list|
|
|
573
|
-
last = list.empty? ? table : list.last + parts[list.size - 1].bytesize
|
|
574
|
-
list << ((last + 7) & ~7)
|
|
575
|
-
}
|
|
576
|
-
size = offsets.last + parts.last.bytesize
|
|
577
|
-
Fiddle::Pointer.malloc(size, Fiddle::RUBY_FREE) do |block|
|
|
578
|
-
slots = parts.zip(offsets).map { |part, offset|
|
|
579
|
-
block[offset, part.bytesize] = part
|
|
580
|
-
block + offset
|
|
581
|
-
}
|
|
582
|
-
block[0, table] = slots.map(&:to_i).pack("Q*")
|
|
583
|
-
yield block, slots
|
|
584
|
-
end
|
|
585
|
-
end
|
|
586
|
-
|
|
587
563
|
# The xfer tier transfers a box rather than the whole view. The box
|
|
588
564
|
# is the loop range grown by how far the kernel reaches from the cell it
|
|
589
565
|
# is on -- per array, because two arrays in one kernel need not be read
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
require "fiddle"
|
|
2
|
+
require "fiddle/import"
|
|
3
|
+
require "carray/jit/errors"
|
|
4
|
+
|
|
5
|
+
class CArray
|
|
6
|
+
module JIT
|
|
7
|
+
|
|
8
|
+
# Everything this library asks of the layer that reaches C from Ruby, and
|
|
9
|
+
# the one place that layer is named. The rest of the library says what
|
|
10
|
+
# it wants -- open this object, find this symbol, make a function of
|
|
11
|
+
# these types, read these bytes -- in the words below, and only this
|
|
12
|
+
# file knows that Fiddle is what answers.
|
|
13
|
+
#
|
|
14
|
+
# That is so the layer can be replaced by changing one file. Fiddle does
|
|
15
|
+
# not let a call be made from a Ractor other than the main one, and a
|
|
16
|
+
# layer that does would be a second version of this module rather than a
|
|
17
|
+
# second version of the library (devel/PROPOSAL_FFI_SEAM.md).
|
|
18
|
+
#
|
|
19
|
+
# What is asked:
|
|
20
|
+
#
|
|
21
|
+
# - a shared object opened, and the libraries already loaded searched
|
|
22
|
+
# - a symbol's address found in one
|
|
23
|
+
# - a function made from an address and its types, and called
|
|
24
|
+
# - bytes read and written at an address
|
|
25
|
+
# - a Ruby object handed to C as the VALUE it is
|
|
26
|
+
#
|
|
27
|
+
# A function is called by calling what #function returns: its #call is
|
|
28
|
+
# the layer's own, so nothing stands between a kernel and the C beyond
|
|
29
|
+
# what the layer itself costs. The arguments it takes for a `:pointer`
|
|
30
|
+
# are an Integer address, a String -- whose bytes are what C is handed --
|
|
31
|
+
# or nil.
|
|
32
|
+
# @private
|
|
33
|
+
module Native
|
|
34
|
+
|
|
35
|
+
# The C types a function is made with, by this library's names for
|
|
36
|
+
# them. `:pointer` is any pointer; a function's arguments say what it
|
|
37
|
+
# points at only to the C, which is all that needs to know.
|
|
38
|
+
TYPES = {
|
|
39
|
+
void: Fiddle::TYPE_VOID,
|
|
40
|
+
pointer: Fiddle::TYPE_VOIDP,
|
|
41
|
+
char: Fiddle::TYPE_CHAR,
|
|
42
|
+
uchar: Fiddle::TYPE_UCHAR,
|
|
43
|
+
short: Fiddle::TYPE_SHORT,
|
|
44
|
+
ushort: Fiddle::TYPE_USHORT,
|
|
45
|
+
int: Fiddle::TYPE_INT,
|
|
46
|
+
uint: Fiddle::TYPE_UINT,
|
|
47
|
+
long: Fiddle::TYPE_LONG,
|
|
48
|
+
ulong: Fiddle::TYPE_ULONG,
|
|
49
|
+
long_long: Fiddle::TYPE_LONG_LONG,
|
|
50
|
+
ulong_long: Fiddle::TYPE_ULONG_LONG,
|
|
51
|
+
float: Fiddle::TYPE_FLOAT,
|
|
52
|
+
double: Fiddle::TYPE_DOUBLE,
|
|
53
|
+
}.freeze
|
|
54
|
+
|
|
55
|
+
# The other way round, for what Fiddle's parser answers. Every code it
|
|
56
|
+
# gives for a spelling CDeclaration hands it is one of the fourteen
|
|
57
|
+
# above: `size_t` and the other platform words arrive as the code of
|
|
58
|
+
# whichever of these they are on this machine.
|
|
59
|
+
NAMES = TYPES.invert.freeze
|
|
60
|
+
private_constant :NAMES
|
|
61
|
+
|
|
62
|
+
POINTER_SIZE = Fiddle::SIZEOF_VOIDP
|
|
63
|
+
|
|
64
|
+
# What a library is, for a caller that was handed one. `jit_extern`'s
|
|
65
|
+
# `from:` takes one of these as it stands.
|
|
66
|
+
Library = Fiddle::Handle
|
|
67
|
+
|
|
68
|
+
# Raised when a library will not open or a symbol is not in it.
|
|
69
|
+
LinkError = Fiddle::DLError
|
|
70
|
+
|
|
71
|
+
PARSER = ::Object.new.extend(Fiddle::CParser)
|
|
72
|
+
private_constant :PARSER
|
|
73
|
+
|
|
74
|
+
# Opens a shared object. `global:` is whether its symbols join the
|
|
75
|
+
# process-wide namespace -- RTLD_GLOBAL -- and it is a required word
|
|
76
|
+
# because layers differ on what they do unasked: Fiddle opens globally,
|
|
77
|
+
# and a layer that opened locally would change which definition a name
|
|
78
|
+
# in one generated object resolves to (see Compiler's link flags).
|
|
79
|
+
# Fiddle has no word for the other way, and nothing here asks for it.
|
|
80
|
+
def self.open_library (path, global:)
|
|
81
|
+
raise ArgumentError, "Fiddle opens a library only globally" unless global
|
|
82
|
+
Fiddle::Handle.new(path, Fiddle::RTLD_LAZY | Fiddle::RTLD_GLOBAL)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# What the process has already loaded, searched as one library.
|
|
86
|
+
def self.loaded_libraries
|
|
87
|
+
Fiddle::Handle::DEFAULT
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# The address of `name` in `library`, as an Integer.
|
|
91
|
+
def self.address (library, name)
|
|
92
|
+
library[name]
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# A function at `address` taking `arguments` and returning `result`,
|
|
96
|
+
# each one of the names in TYPES.
|
|
97
|
+
#
|
|
98
|
+
# `release_gvl:` is whether the GVL is let go for the length of a call,
|
|
99
|
+
# and it is a required word because layers differ on what they do
|
|
100
|
+
# unasked -- Fiddle lets it go, FFI keeps it. Letting it go is right
|
|
101
|
+
# for C that touches no Ruby value, which is every generated kernel; it
|
|
102
|
+
# is wrong for C that does, which is CArray's own sweep.
|
|
103
|
+
def self.function (address, arguments, result, release_gvl:, name: nil)
|
|
104
|
+
types = arguments.map { |type| TYPES.fetch(type) }
|
|
105
|
+
options = name ? { name: name } : {}
|
|
106
|
+
return Fiddle::Function.new(address, types, TYPES.fetch(result), **options) if release_gvl
|
|
107
|
+
begin
|
|
108
|
+
Fiddle::Function.new(address, types, TYPES.fetch(result),
|
|
109
|
+
need_gvl: true, **options)
|
|
110
|
+
rescue ArgumentError
|
|
111
|
+
# A Fiddle too old to be told. It is also too old to have let go
|
|
112
|
+
# of the GVL in the first place -- the keyword and the releasing
|
|
113
|
+
# arrived together -- so what is left here is what that Fiddle
|
|
114
|
+
# would have done anyway.
|
|
115
|
+
Fiddle::Function.new(address, types, TYPES.fetch(result), **options)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# The type a C spelling is on this machine, as a name in TYPES, or nil
|
|
120
|
+
# where it is not one. The spelling is one CDeclaration has already
|
|
121
|
+
# put in order: `unsigned long` rather than `long unsigned`, `void *`
|
|
122
|
+
# for any pointer.
|
|
123
|
+
def self.c_type (spelling)
|
|
124
|
+
NAMES.fetch(PARSER.parse_ctype(spelling))
|
|
125
|
+
rescue StandardError
|
|
126
|
+
nil
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# `size` bytes at `address`, as a String. Fiddle::Pointer.read and
|
|
130
|
+
# .write are not in the fiddle Ruby 3.2 ships (1.1.1).
|
|
131
|
+
def self.read (address, size)
|
|
132
|
+
Fiddle::Pointer.new(address)[0, size]
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# `bytes` written at `address`.
|
|
136
|
+
def self.write (address, bytes)
|
|
137
|
+
Fiddle::Pointer.new(address)[0, bytes.bytesize] = bytes
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# `object` as the VALUE C sees -- for CArray's own C, which takes the
|
|
141
|
+
# array itself rather than its memory.
|
|
142
|
+
def self.value (object)
|
|
143
|
+
Fiddle.dlwrap(object)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
end
|
|
149
|
+
end
|