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.
@@ -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 "fiddle"
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. Fiddle.dlopen passes RTLD_GLOBAL, so on ELF those
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 17 KB, so the default is
87
- # roughly 9 MB -- far more than any real program compiles, but a bound
88
- # all the same.
89
- DEFAULT_ENTRY_LIMIT = 512
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 [Fiddle handle, whether it was compiled rather than reused].
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. The staging path a build
235
- # compiles to is named for the process, so two threads building at
236
- # once wrote the same file and whichever renamed first left the other
237
- # renaming a path that was gone -- a bare Errno::ENOENT out of three
238
- # threads in four. Serialising them also spares the second thread the
239
- # build: it takes the lock after the first has renamed, and finds the
240
- # object where a later run would. Reentrant, so that a build reached
241
- # from inside another -- which none is today -- would not stop here.
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
- File.write(source_path, header.to_s + source)
270
- # Build to a unique path and rename, so a concurrent process never
271
- # loads a half-written object.
272
- staging_path = "#{object_path}.#{Process.pid}"
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
- # A process killed mid-compile leaves `<key>.bundle.<pid>` behind.
473
- # Those match neither the entry glob nor the prune glob, so without
474
- # this they would sit in the cache for good. Only swept on a miss,
475
- # to keep it off the reuse path.
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, "*#{shared_object_suffix}.*")].each do |path|
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
- Fiddle.dlopen(path)
576
- rescue Fiddle::DLError
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 << Fiddle::Pointer[dim] if walks_axes?(plan, strided)
88
- args << Fiddle::Pointer[steps] unless strided.empty?
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
- Fiddle::Function.new(handle["carray_jit_expression"],
131
- [Fiddle::TYPE_LONG_LONG] +
132
- [Fiddle::TYPE_VOIDP] * (1 + arity(plan, strided)),
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} = #{literal(node)};",
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
- fill = CArray::Fusion::Const.new(node.fill, node.data_type)
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
@@ -1,4 +1,4 @@
1
- require "fiddle"
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 Fiddle::Function shape is
11
- # fixed and the per-kernel detail travels in the six buffers.
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 = Fiddle::Function.new(@handle[CGenerator::FUNCTION_NAME],
130
- [Fiddle::TYPE_VOIDP] * 10,
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
- held = [buffer(reals), buffer(integers), buffer(functions),
237
- buffer(data), masks, masks, error]
238
- error = in_one_block(held) { |context, slots|
239
- Sweep.call(slab_pointer, fsync, arrays, context)
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 Fiddle lets go of the GVL for the call, so two
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[SLAB_NAME]
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
- Fiddle::Function.new(@handle[CGenerator::BORDER_NAME],
468
- [Fiddle::TYPE_VOIDP] * 10, Fiddle::TYPE_VOID)
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.fiddle)
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 Fiddle can hand over,
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