carray 3.0.2 → 3.0.4

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.
Files changed (247) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +568 -0
  3. data/README.md +6 -6
  4. data/carray.gemspec +1 -1
  5. data/ext/ca_axis_descriptor.h +27 -24
  6. data/ext/ca_axis_dispatch.c +220 -0
  7. data/ext/ca_axis_group.c +201 -123
  8. data/ext/ca_binop_dispatch.c +46 -52
  9. data/ext/ca_binop_dispatch.h +1 -1
  10. data/ext/ca_categorical_iterator.c +249 -183
  11. data/ext/ca_compare.c +7 -12
  12. data/ext/ca_for_buffer.h +21 -68
  13. data/ext/ca_group_iter.c +38 -23
  14. data/ext/ca_kernel_iterator.c +1765 -2076
  15. data/ext/ca_kernel_iterator.h +584 -566
  16. data/ext/ca_monop_dispatch.c +86 -63
  17. data/ext/ca_monop_dispatch.h +5 -0
  18. data/ext/ca_obj_array.c +223 -56
  19. data/ext/ca_obj_bincmp.c +172 -138
  20. data/ext/ca_obj_binop.c +116 -230
  21. data/ext/ca_obj_bitarray.c +40 -26
  22. data/ext/ca_obj_bitfield.c +100 -68
  23. data/ext/ca_obj_block.c +19 -9
  24. data/ext/ca_obj_byte_swap.c +45 -46
  25. data/ext/ca_obj_const_string.c +269 -44
  26. data/ext/ca_obj_face.c +612 -89
  27. data/ext/ca_obj_face.h +111 -47
  28. data/ext/ca_obj_fake.c +36 -79
  29. data/ext/ca_obj_farray.c +7 -3
  30. data/ext/ca_obj_field.c +33 -8
  31. data/ext/ca_obj_fixlen_string.c +23 -24
  32. data/ext/ca_obj_grid.c +541 -210
  33. data/ext/ca_obj_meld.c +331 -111
  34. data/ext/ca_obj_moncmp.c +80 -65
  35. data/ext/ca_obj_monop.c +177 -149
  36. data/ext/ca_obj_object.c +664 -248
  37. data/ext/ca_obj_record.c +63 -23
  38. data/ext/ca_obj_reduce.c +18 -16
  39. data/ext/ca_obj_refer.c +140 -58
  40. data/ext/ca_obj_remap.c +85 -24
  41. data/ext/ca_obj_repeat.c +24 -9
  42. data/ext/ca_obj_roll.c +121 -16
  43. data/ext/ca_obj_select.c +61 -58
  44. data/ext/ca_obj_select_axis.c +73 -107
  45. data/ext/ca_obj_shift.c +32 -102
  46. data/ext/ca_obj_stack.c +372 -122
  47. data/ext/ca_obj_stride.c +348 -140
  48. data/ext/ca_obj_string.c +88 -22
  49. data/ext/ca_obj_tile.c +38 -21
  50. data/ext/ca_obj_time.c +45 -31
  51. data/ext/ca_obj_timedelta.c +43 -29
  52. data/ext/ca_obj_transpose.c +17 -6
  53. data/ext/ca_obj_triop.c +125 -141
  54. data/ext/ca_obj_window.c +330 -44
  55. data/ext/ca_op_ipower.c +24 -11
  56. data/ext/ca_op_powi.h +57 -30
  57. data/ext/ca_pow5_table.h +656 -0
  58. data/ext/ca_sort_kernels.h +70 -0
  59. data/ext/ca_sweep_engine.c +104 -81
  60. data/ext/ca_sweep_engine.h +7 -20
  61. data/ext/ca_triop_dispatch.c +4 -1
  62. data/ext/ca_triop_dispatch.h +5 -2
  63. data/ext/caframe_csv_reader.c +542 -0
  64. data/ext/carray.h +241 -92
  65. data/ext/carray_access.c +608 -276
  66. data/ext/carray_address_basis.c +494 -97
  67. data/ext/carray_attribute.c +129 -4
  68. data/ext/carray_bincount.c +39 -7
  69. data/ext/carray_broadcast.c +56 -19
  70. data/ext/carray_build_flags.h +1 -1
  71. data/ext/carray_call_cfunc.c +108 -28
  72. data/ext/carray_cast.c +1576 -223
  73. data/ext/carray_cast_func.rb +77 -24
  74. data/ext/carray_class.c +4 -3
  75. data/ext/carray_conversion.c +208 -75
  76. data/ext/carray_copy.c +82 -24
  77. data/ext/carray_core.c +646 -162
  78. data/ext/carray_count.c +171 -88
  79. data/ext/carray_element.c +94 -60
  80. data/ext/carray_factorize.c +670 -324
  81. data/ext/carray_generate.c +260 -83
  82. data/ext/carray_histogram.c +209 -106
  83. data/ext/carray_hold.c +14 -16
  84. data/ext/carray_index_classifier.c +422 -243
  85. data/ext/carray_internal.h +692 -3
  86. data/ext/carray_kernels_bincmp.c +7 -0
  87. data/ext/carray_kernels_binop.c +487 -120
  88. data/ext/carray_kernels_init.c +653 -50
  89. data/ext/carray_kernels_moncmp.c +15 -8
  90. data/ext/carray_kernels_monop.c +99 -43
  91. data/ext/carray_kernels_reduce_aggregate.c +3925 -6112
  92. data/ext/carray_kernels_reduce_boolean.c +112 -36
  93. data/ext/carray_kernels_reduce_cumulative.c +2127 -6903
  94. data/ext/carray_kernels_reduce_extreme.c +3032 -2892
  95. data/ext/carray_kernels_reduce_variance.c +439 -500
  96. data/ext/carray_kernels_scan.c +1097 -488
  97. data/ext/carray_kernels_search.c +3787 -2176
  98. data/ext/carray_kernels_sort.c +1200 -920
  99. data/ext/carray_kernels_triop.c +479 -7
  100. data/ext/carray_key_windows.c +216 -0
  101. data/ext/carray_lazy.c +967 -153
  102. data/ext/carray_loop.c +81 -65
  103. data/ext/carray_mask.c +910 -324
  104. data/ext/carray_math_kernel.h +1 -0
  105. data/ext/carray_median_percentile.c +299 -100
  106. data/ext/carray_memory_view.c +872 -758
  107. data/ext/carray_operator.c +1171 -364
  108. data/ext/carray_order.c +505 -133
  109. data/ext/carray_partition.c +112 -65
  110. data/ext/carray_random.c +327 -239
  111. data/ext/carray_round.c +273 -0
  112. data/ext/carray_scatter.c +346 -78
  113. data/ext/carray_segment.c +257 -0
  114. data/ext/carray_slab.c +249 -86
  115. data/ext/carray_sort.c +224 -227
  116. data/ext/carray_sort_kernel.c +8 -1
  117. data/ext/carray_struct.c +118 -39
  118. data/ext/carray_test.c +193 -69
  119. data/ext/carray_time_text.c +1182 -0
  120. data/ext/carray_undef.c +4 -0
  121. data/ext/carray_utils.c +579 -113
  122. data/ext/extconf.rb +27 -2
  123. data/ext/mk_call_cfunc.rb +19 -4
  124. data/ext/mk_pow5_table.rb +35 -0
  125. data/ext/mkkernel.rb +1202 -1014
  126. data/ext/ruby_carray.c +17 -5
  127. data/ext/version.h +4 -4
  128. data/ext/version.rb +4 -2
  129. data/lib/carray/attribute.rb +160 -17
  130. data/lib/carray/autoload_carray.rb +23 -27
  131. data/lib/carray/autoload_method_extension.rb +20 -5
  132. data/lib/carray/axis_group.rb +360 -63
  133. data/lib/carray/basics.rb +162 -28
  134. data/lib/carray/bincount_nd.rb +18 -2
  135. data/lib/carray/block_iterator.rb +231 -58
  136. data/lib/carray/boolean_reduce.rb +34 -42
  137. data/lib/carray/categorical.rb +242 -52
  138. data/lib/carray/categorical_iterator.rb +437 -827
  139. data/lib/carray/complex.rb +41 -34
  140. data/lib/carray/conditional.rb +150 -14
  141. data/lib/carray/const_string.rb +59 -4
  142. data/lib/carray/construct.rb +15 -11
  143. data/lib/carray/data_type_extension.rb +1 -0
  144. data/lib/carray/distance_limit.rb +77 -0
  145. data/lib/carray/fixlen_string.rb +7 -1
  146. data/lib/carray/frame/column_verbs.rb +70 -0
  147. data/lib/carray/frame/concat.rb +157 -6
  148. data/lib/carray/frame/convert.rb +82 -7
  149. data/lib/carray/frame/csv_parser.rb +688 -87
  150. data/lib/carray/frame/describe.rb +80 -0
  151. data/lib/carray/frame/frame.rb +369 -45
  152. data/lib/carray/frame/group.rb +631 -90
  153. data/lib/carray/frame/io.rb +506 -79
  154. data/lib/carray/frame/join.rb +411 -62
  155. data/lib/carray/frame/records.rb +116 -19
  156. data/lib/carray/frame/reshape.rb +455 -0
  157. data/lib/carray/frame/sort.rb +30 -18
  158. data/lib/carray/frame/verbs.rb +868 -61
  159. data/lib/carray/frame/windows.rb +117 -0
  160. data/lib/carray/frame.rb +4 -0
  161. data/lib/carray/fuse_source.rb +118 -2
  162. data/lib/carray/fusion.rb +257 -42
  163. data/lib/carray/histogram.rb +6 -1
  164. data/lib/carray/inspect.rb +54 -13
  165. data/lib/carray/iterator.rb +99 -9
  166. data/lib/carray/key_windows.rb +620 -0
  167. data/lib/carray/lazy.rb +216 -193
  168. data/lib/carray/mask_gap_fill.rb +112 -92
  169. data/lib/carray/math.rb +32 -19
  170. data/lib/carray/meld_reduce.rb +52 -21
  171. data/lib/carray/methods/align_addr.rb +8 -4
  172. data/lib/carray/methods/bin.rb +26 -18
  173. data/lib/carray/methods/bincount.rb +18 -2
  174. data/lib/carray/methods/bit_string.rb +15 -4
  175. data/lib/carray/methods/choose.rb +16 -3
  176. data/lib/carray/methods/composition.rb +18 -10
  177. data/lib/carray/methods/factorize.rb +2 -2
  178. data/lib/carray/methods/gather_nd.rb +25 -5
  179. data/lib/carray/methods/insert_block.rb +9 -14
  180. data/lib/carray/methods/is_between.rb +59 -0
  181. data/lib/carray/methods/is_in.rb +140 -10
  182. data/lib/carray/methods/join.rb +2 -7
  183. data/lib/carray/methods/lag.rb +49 -0
  184. data/lib/carray/methods/locate_addr.rb +182 -45
  185. data/lib/carray/methods/mode.rb +18 -1
  186. data/lib/carray/methods/nunique.rb +11 -6
  187. data/lib/carray/methods/repeat.rb +1 -20
  188. data/lib/carray/methods/resize.rb +21 -23
  189. data/lib/carray/methods/snap.rb +34 -12
  190. data/lib/carray/methods/unique.rb +11 -2
  191. data/lib/carray/methods/value_counts.rb +4 -11
  192. data/lib/carray/record_order.rb +208 -0
  193. data/lib/carray/rng.rb +3 -0
  194. data/lib/carray/runtime.rb +65 -17
  195. data/lib/carray/segment_iterator.rb +1221 -0
  196. data/lib/carray/serialize.rb +214 -30
  197. data/lib/carray/slab_iterator.rb +101 -27
  198. data/lib/carray/stack.rb +10 -4
  199. data/lib/carray/string.rb +105 -5
  200. data/lib/carray/string_operation_extension.rb +12 -4
  201. data/lib/carray/struct.rb +137 -58
  202. data/lib/carray/struct_builder.rb +124 -17
  203. data/lib/carray/time.rb +1222 -186
  204. data/lib/carray/window_iterator.rb +242 -81
  205. data/lib/carray.rb +9 -2
  206. data/yard-stubs/ca_obj_array.rb +4 -4
  207. data/yard-stubs/ca_obj_bitarray.rb +7 -0
  208. data/yard-stubs/ca_obj_bitfield.rb +23 -12
  209. data/yard-stubs/ca_obj_fake.rb +1 -1
  210. data/yard-stubs/ca_obj_field.rb +5 -1
  211. data/yard-stubs/ca_obj_grid.rb +21 -10
  212. data/yard-stubs/ca_obj_refer.rb +5 -4
  213. data/yard-stubs/ca_obj_roll.rb +2 -3
  214. data/yard-stubs/ca_obj_shift.rb +5 -1
  215. data/yard-stubs/ca_obj_stride.rb +10 -7
  216. data/yard-stubs/ca_obj_tile.rb +1 -1
  217. data/yard-stubs/ca_obj_transpose.rb +1 -1
  218. data/yard-stubs/ca_obj_window.rb +9 -5
  219. data/yard-stubs/carray_access.rb +11 -5
  220. data/yard-stubs/carray_attribute.rb +0 -5
  221. data/yard-stubs/carray_broadcast.rb +1 -1
  222. data/yard-stubs/carray_cast.rb +88 -43
  223. data/yard-stubs/carray_conversion.rb +15 -2
  224. data/yard-stubs/carray_copy.rb +12 -5
  225. data/yard-stubs/carray_core.rb +3 -3
  226. data/yard-stubs/carray_count.rb +9 -0
  227. data/yard-stubs/carray_element.rb +10 -4
  228. data/yard-stubs/carray_generate.rb +21 -1
  229. data/yard-stubs/carray_lazy.rb +5 -1
  230. data/yard-stubs/carray_loop.rb +4 -1
  231. data/yard-stubs/carray_mask.rb +60 -12
  232. data/yard-stubs/carray_math.rb +52 -14
  233. data/yard-stubs/carray_median_percentile.rb +15 -8
  234. data/yard-stubs/carray_memory_view.rb +40 -23
  235. data/yard-stubs/carray_order.rb +61 -29
  236. data/yard-stubs/carray_random.rb +13 -4
  237. data/yard-stubs/carray_scatter.rb +21 -9
  238. data/yard-stubs/carray_segment.rb +72 -0
  239. data/yard-stubs/carray_slab.rb +10 -2
  240. data/yard-stubs/carray_sort.rb +49 -8
  241. data/yard-stubs/carray_test.rb +15 -3
  242. data/yard-stubs/carray_utils.rb +0 -26
  243. metadata +19 -5
  244. data/ext/ca_for_each_element.h +0 -239
  245. data/ext/carray_kernels_map.c +0 -3467
  246. data/lib/carray/arrow_tensor.rb +0 -401
  247. data/lib/carray/table.rb +0 -108
@@ -16,8 +16,11 @@
16
16
  - CArray.wrap_memory_view (zero-copy; contiguous only)
17
17
 
18
18
  Format-string contract:
19
- - Producer emits PEP 3118 strict; consumer is Postel-permissive
20
- (accepts synonyms other producers emit).
19
+ - Producer emits Ruby's vocabulary (ruby/memory_view.h), records
20
+ included; only the types Ruby cannot spell ('?', 'Zf' / 'Zd', "Ns")
21
+ stay PEP 3118. The consumer is Postel-permissive (accepts synonyms
22
+ other producers emit). A record's member names, which Ruby's format
23
+ cannot carry, are CARecord#memory_view_field_names.
21
24
  - See docs/interop/MemoryViewFormat.md for the format-string contract.
22
25
  - See devel/DESIGN_MemoryView.md for the design rationale.
23
26
  - See docs/interop/MemoryView.md for the user-facing MV API.
@@ -26,11 +29,16 @@
26
29
 
27
30
  #include "carray.h"
28
31
  #include "ca_obj_face.h" /* ca_strip_face for Face producers */
32
+ #include "carray_internal.h"
29
33
  #include "ruby/memory_view.h"
34
+ #include "ruby/st.h"
35
+ #include "ruby/thread_native.h"
36
+ #include <ctype.h>
30
37
  #include <string.h>
31
38
 
32
- /* private_data is not used: ca_mv_get is alias-only and ca_mv_release
33
- needs no per-view state. Kept as a NULL field for API compatibility. */
39
+ /* private_data holds the view's own copy of a record format (see ca_mv_get)
40
+ and is NULL otherwise; ca_mv_release frees it. No other per-view state:
41
+ ca_mv_get is alias-only. */
34
42
 
35
43
  /* ---------------- forward declarations for non-public structs ----------------
36
44
  CABlock, CARefer, CARepeat are declared in carray.h. CAFarray
@@ -55,12 +63,10 @@ typedef struct {
55
63
 
56
64
  /* ---------------- obj_type strategy ---------------- */
57
65
 
58
- /* Strategy = label only; ca_mv_get is alias-only and all CAStride-family
59
- strategies share the same code path (compose-to-root + entity->ptr +
60
- composed_strides). The labels are kept for ca_mv_runtime_types[]
61
- readability and reject diagnostics. CA_MV_ATTACH stays defined but is
62
- structurally unreachable -- ca_mv_check_alias_chain rejects every
63
- obj_type that maps to it. */
66
+ /* Strategy = label only. The export is alias-only and is decided by
67
+ ca_mv_compose_to_entity alone; the labels say which classes get a
68
+ MemoryView registration and keep ca_mv_runtime_types[] readable.
69
+ Every class labelled CA_MV_ATTACH is refused by that resolver. */
64
70
  typedef enum {
65
71
  CA_MV_REJECT,
66
72
  CA_MV_DIRECT, /* entity / wrap / scalar; ca->ptr valid as-is */
@@ -72,14 +78,11 @@ typedef enum {
72
78
  CA_MV_ATTACH, /* unreachable: rejected by alias-chain check */
73
79
  } ca_mv_strategy_t;
74
80
 
75
- /* Resolved at Init_carray_memory_view. The order is the same as
76
- ca_mv_runtime_types[] below. Runtime obj_type ids are stored here so
77
- ca_mv_strategy_for can match them with a simple linear scan. */
81
+ /* Resolved at Init_carray_memory_view. Runtime obj_type ids are stored
82
+ here so ca_mv_reject_reason_for can name the class it refuses. */
78
83
  static ca_mv_runtime_type_t ca_mv_runtime_types[CA_MV_NUM_RUNTIME_OBJ_TYPES] = {
79
84
  { "CAFarray", "CA_OBJ_FARRAY", -1, Qnil, /* CA_MV_FARRAY */ 4 },
80
85
  { "CATranspose", "CA_OBJ_TRANSPOSE", -1, Qnil, /* CA_MV_TRANS */ 5 },
81
- /* CAMapping is retired; a[mapper] now builds a CAGrid/CAStride chain
82
- whose layers are covered above. */
83
86
  { "CAGrid", "CA_OBJ_GRID", -1, Qnil, /* CA_MV_ATTACH */ 7 },
84
87
  { "CAShift", "CA_OBJ_SHIFT", -1, Qnil, /* CA_MV_ATTACH */ 7 },
85
88
  { "CAWindow", "CA_OBJ_WINDOW", -1, Qnil, /* CA_MV_ATTACH */ 7 },
@@ -143,69 +146,69 @@ ca_mv_format_for (int8_t data_type)
143
146
  }
144
147
  }
145
148
 
146
- /* PEP 3118 single-character specifier for a primitive data_type, for
147
- use inside a `T{...}` struct format body. This is the context the
148
- distinct entry point was kept for: `T{...}` is a PEP 3118 construct
149
- with no Ruby counterpart, so a record body stays in PEP's vocabulary
150
- even though the top level now publishes Ruby's. A consumer reading
151
- the body reads it as PEP, and a bridge that forwards the record
152
- whole -- as pycall-memoryview does -- keeps it intact.
153
-
154
- Returns NULL for non-primitive (CA_FIXLEN / CA_OBJECT) or for the
155
- retired float128 / cmplx256 enum slots. */
156
- static const char *
157
- ca_mv_pep3118_char (int8_t data_type)
158
- {
159
- switch (data_type) {
160
- case CA_INT8: return "b";
161
- case CA_UINT8: return "B";
162
- case CA_INT16: return "h";
163
- case CA_UINT16: return "H";
164
- default: return ca_mv_format_for(data_type);
165
- }
166
- }
167
-
168
- /* Build (or fetch from cache) the PEP 3118 struct format string for
169
- a data_class. Walks MEMBER_TABLE in MEMBERS order and emits
170
- "T{<fmt>:<name>:<fmt>:<name>:}". Returns Qnil if the class
171
- contains a member type the producer cannot express (nested
172
- sub-struct class, CArray template, :fixlen, :object).
173
-
174
- The result is frozen and cached on the data_class via
175
- @__mv_struct_format__ so successive calls are O(1) and the
176
- underlying RSTRING_PTR is stable (the cached String is rooted by
177
- the class, which outlives any MV produced from it). */
178
- /* Helper: resolve a MEMBER_TABLE member-type slot to (fmt, bytes).
179
- Returns 1 on success, 0 if the type is not expressible in PEP 3118
180
- struct format (nested class, CArray template, :fixlen, :object,
181
- :bitfield). Does NOT raise — :bitfield in particular is checked
182
- before rb_ca_guess_type so the struct-format walk can give up
183
- gracefully on bit-bearing structs. */
149
+ /* The Ruby MemoryView specifier for one record member, written into `out`
150
+ with its byte order as ruby/memory_view.h writes it: a suffix ('<' / '>')
151
+ on an integer, its own letter on a float ('e' / 'E' little, 'g' / 'G'
152
+ big). A member without a declared byte order is in the machine's.
153
+ Returns 0 for a member the format has no item for: a nested struct, an
154
+ array member, :fixlen, :object, a bit member, or a complex member with a
155
+ declared byte order (Ruby has no complex specifier to suffix). */
184
156
  static int
185
- ca_mv_struct_member_resolve (VALUE info, const char **fmt_out, ca_size_t *bytes_out)
157
+ ca_mv_record_member_spec (VALUE info, char *out, size_t outlen,
158
+ ca_size_t *bytes_out)
186
159
  {
187
160
  VALUE type = rb_ary_entry(info, 1);
161
+ VALUE opts = rb_ary_entry(info, 2);
162
+ VALUE endian = Qnil;
163
+ const char *base;
164
+ int8_t dt;
188
165
  if (!SYMBOL_P(type)) return 0;
189
166
  ID type_id = SYM2ID(type);
190
- /* Reject explicitly unsupported symbol types up front (so
191
- rb_ca_guess_type never sees them and never raises). */
192
167
  if (type_id == rb_intern("bitfield") ||
193
168
  type_id == rb_intern("fixlen") ||
194
169
  type_id == rb_intern("object")) {
195
170
  return 0;
196
171
  }
197
- int8_t dt = rb_ca_guess_type(rb_sym2str(type));
198
- const char *fmt = ca_mv_pep3118_char(dt);
199
- if (!fmt) return 0;
200
- *fmt_out = fmt;
172
+ dt = rb_ca_guess_type(rb_sym2str(type));
173
+ base = ca_mv_format_for(dt);
174
+ if (!base) return 0;
175
+ if (RB_TYPE_P(opts, T_HASH)) {
176
+ endian = rb_hash_aref(opts, ID2SYM(rb_intern("endian")));
177
+ }
178
+ if ( SYMBOL_P(endian) && ( SYM2ID(endian) == rb_intern("big")
179
+ || SYM2ID(endian) == rb_intern("little") ) ) {
180
+ int big = ( SYM2ID(endian) == rb_intern("big") );
181
+ switch (dt) {
182
+ case CA_FLOAT32: base = big ? "g" : "e"; break;
183
+ case CA_FLOAT64: base = big ? "G" : "E"; break;
184
+ case CA_CMPLX64: case CA_CMPLX128: return 0;
185
+ case CA_BOOLEAN: case CA_INT8: case CA_UINT8: break; /* one byte */
186
+ default:
187
+ snprintf(out, outlen, "%s%s", base, big ? ">" : "<");
188
+ *bytes_out = (ca_size_t) ca_sizeof[dt];
189
+ return 1;
190
+ }
191
+ }
192
+ snprintf(out, outlen, "%s", base);
201
193
  *bytes_out = (ca_size_t) ca_sizeof[dt];
202
194
  return 1;
203
195
  }
204
196
 
197
+ /* Build (or fetch from cache) the Ruby MemoryView format of a record: the
198
+ members' specifiers in their byte order in the record, with "x<N>" for
199
+ the padding between them and after the last. Ruby's format has no
200
+ member names; the record array gives them as #memory_view_field_names,
201
+ one per specifier. Returns Qnil for a record the format cannot describe:
202
+ a member it has no item for (see above), or members that overlap or come
203
+ out of offset order (a union).
204
+
205
+ The result is frozen and cached on the data_class. It is an ordinary
206
+ String that compaction can move, so a view takes a copy of its bytes
207
+ rather than its pointer (ca_mv_get). */
205
208
  static VALUE
206
209
  ca_mv_struct_format_for_data_class (VALUE data_class)
207
210
  {
208
- ID iv = rb_intern("@__mv_struct_format__");
211
+ ID iv = rb_intern("@__mv_record_format__");
209
212
  if (rb_ivar_defined(data_class, iv)) {
210
213
  VALUE cached = rb_ivar_get(data_class, iv);
211
214
  /* Qfalse is a sentinel meaning "tried, found unsupported". */
@@ -216,61 +219,36 @@ ca_mv_struct_format_for_data_class (VALUE data_class)
216
219
  VALUE table = rb_const_get(data_class, rb_intern("MEMBER_TABLE"));
217
220
  long n = RARRAY_LEN(members);
218
221
  long i;
219
- VALUE buf = rb_str_buf_new(2 + 6 * n);
222
+ VALUE buf = rb_str_buf_new(4 * n + 8);
220
223
  ca_size_t cursor = 0;
221
224
  ca_size_t data_size = NUM2SIZE(rb_const_get(data_class, rb_intern("DATA_SIZE")));
222
-
223
- rb_str_buf_cat_ascii(buf, "T{");
225
+ char spec[16], pad[32];
224
226
 
225
227
  for (i = 0; i < n; i++) {
226
- VALUE name = rb_ary_entry(members, i);
227
- VALUE info = rb_hash_aref(table, name);
228
+ VALUE info = rb_hash_aref(table, rb_ary_entry(members, i));
229
+ ca_size_t offset, mb;
228
230
  if (NIL_P(info)) goto unsupported;
229
- ca_size_t offset = NUM2SIZE(rb_ary_entry(info, 0));
230
-
231
- const char *fmt;
232
- ca_size_t mb;
233
- if (!ca_mv_struct_member_resolve(info, &fmt, &mb)) goto unsupported;
234
-
235
- /* If alignment or explicit padding has left a gap before this
236
- member, emit "<N>x:" to account for it. PEP 3118 uses 'x'
237
- as the pad-byte specifier. The canonical form elides the name
238
- slot after the pad spec (padding is anonymous by definition);
239
- consumers MUST accept both elided and named (legacy "<N>x:_:")
240
- forms. */
231
+ offset = NUM2SIZE(rb_ary_entry(info, 0));
232
+ if (offset < cursor) goto unsupported;
233
+ if (!ca_mv_record_member_spec(info, spec, sizeof(spec), &mb)) goto unsupported;
241
234
  if (offset > cursor) {
242
- char pad_decl[32];
243
- snprintf(pad_decl, sizeof(pad_decl), "%lldx:",
244
- (long long) (offset - cursor));
245
- rb_str_buf_cat_ascii(buf, pad_decl);
246
- cursor = offset;
235
+ snprintf(pad, sizeof(pad), "x%lld", (long long) (offset - cursor));
236
+ rb_str_buf_cat_ascii(buf, pad);
247
237
  }
248
-
249
- rb_str_buf_cat_ascii(buf, fmt);
250
- rb_str_buf_cat_ascii(buf, ":");
251
- /* MEMBERS entries are strings; emit verbatim. */
252
- rb_str_buf_append(buf, rb_String(name));
253
- rb_str_buf_cat_ascii(buf, ":");
254
- cursor += mb;
238
+ rb_str_buf_cat_ascii(buf, spec);
239
+ cursor = offset + mb;
255
240
  }
256
-
257
- /* Trailing padding (DATA_SIZE > last member's end). Elided form
258
- (see the alignment-gap branch above for rationale). */
241
+ if (cursor > data_size) goto unsupported;
259
242
  if (cursor < data_size) {
260
- char pad_decl[32];
261
- snprintf(pad_decl, sizeof(pad_decl), "%lldx:",
262
- (long long) (data_size - cursor));
263
- rb_str_buf_cat_ascii(buf, pad_decl);
243
+ snprintf(pad, sizeof(pad), "x%lld", (long long) (data_size - cursor));
244
+ rb_str_buf_cat_ascii(buf, pad);
264
245
  }
265
246
 
266
- rb_str_buf_cat_ascii(buf, "}");
267
247
  rb_obj_freeze(buf);
268
248
  rb_ivar_set(data_class, iv, buf);
269
249
  return buf;
270
250
 
271
251
  unsupported:
272
- /* Cache Qfalse to remember "we tried and gave up" — avoids
273
- re-walking MEMBER_TABLE on every wrap call. */
274
252
  rb_ivar_set(data_class, iv, Qfalse);
275
253
  return Qnil;
276
254
  }
@@ -282,7 +260,7 @@ ca_mv_data_type_exportable (int8_t data_type)
282
260
  }
283
261
 
284
262
  /* Same as ca_mv_data_type_exportable, but also accepts CA_FIXLEN:
285
- - with a data_class -> emit PEP 3118 "T{...}" struct format
263
+ - with a data_class -> emit the record's Ruby format (members in order)
286
264
  - without a data_class -> emit PEP 3118 "Ns" fixed-bytes format */
287
265
  static int
288
266
  ca_mv_ca_exportable (CArray *ca, VALUE obj)
@@ -302,33 +280,80 @@ ca_mv_ca_exportable (CArray *ca, VALUE obj)
302
280
  }
303
281
 
304
282
  /* Reverse mapping for the importer (Postel consumer; see
305
- docs/interop/MemoryViewFormat.md for the contract):
306
- - Strip a leading byte-order prefix ('<', '>', '=') if it matches host
307
- endian; reject if it disagrees.
308
- - Strip the alignment modifier '|'.
309
- - Reject endian-bearing primary specifiers ('e','g','E','G','n','v',
310
- 'N','V'); cross-endian sources are not accepted.
311
- - Dispatch on the (stripped_format, item_size) tuple.
312
- - Canonical: '?' for bool, 'Zf' / 'Zd' for complex64/128 (PEP 3118).
283
+ docs/interop/MemoryViewFormat.md for the contract). Both vocabularies
284
+ are read: Ruby's (byte order as a suffix, "i<", "s!>"; '_' for '!';
285
+ 'e' 'E' 'g' 'G' 'n' 'N' 'v' 'V' carrying their own order; 'j' 'J' for
286
+ intptr) and PEP 3118's (order as a prefix '<' '>' '!' '=' '@'; 'b' 'B'
287
+ 'h' 'H' '?'; 'Zf' 'Zd'). The alignment modifier '|' is skipped.
288
+ - CArray stores host order, so an order that disagrees with the host
289
+ is refused. A one-byte type has no order, so any is accepted.
290
+ - Dispatch on the (type, item_size) tuple.
313
291
  - Synonyms accepted but not emitted: 'C'/1 maps to UINT8 (so other
314
292
  producers' bool buffers are received as UINT8); 'ff' / 'dd' map to
315
293
  complex64/128 (producers that split complex into two floats). */
294
+
295
+ /* Byte order named in a format: 0 none, 1 little, 2 big. */
296
+ #define CA_MV_ORDER_NONE 0
297
+ #define CA_MV_ORDER_LITTLE 1
298
+ #define CA_MV_ORDER_BIG 2
299
+
300
+ static int
301
+ ca_mv_order_merge (int a, int b)
302
+ {
303
+ if ( a == CA_MV_ORDER_NONE ) return b;
304
+ if ( b == CA_MV_ORDER_NONE ) return a;
305
+ return ( a == b ) ? a : -1;
306
+ }
307
+
316
308
  static int8_t
317
309
  ca_mv_data_type_from_format (const char *format, ssize_t item_size)
318
310
  {
311
+ const int host = (ca_endian == CA_LITTLE_ENDIAN) ? CA_MV_ORDER_LITTLE
312
+ : CA_MV_ORDER_BIG;
319
313
  const char *fmt = format ? format : "";
320
- /* Strip alignment and host-matching byte-order prefix. */
314
+ char buf[16];
315
+ size_t len;
316
+ int order = CA_MV_ORDER_NONE;
317
+ int one_byte;
318
+
321
319
  if (*fmt == '|') fmt++;
322
- if (*fmt == '<' || *fmt == '>' || *fmt == '=') {
323
- /* The carray runtime stores data in host-endian; cross-endian
324
- sources are rejected. */
325
- int host_is_le = (ca_endian == CA_LITTLE_ENDIAN);
326
- int wants_le = (*fmt == '<') || (*fmt == '=' && host_is_le);
327
- int wants_be = (*fmt == '>') || (*fmt == '=' && !host_is_le);
328
- if ((wants_le && !host_is_le) || (wants_be && host_is_le)) return -1;
329
- fmt++;
320
+ switch (*fmt) {
321
+ case '<': order = CA_MV_ORDER_LITTLE; fmt++; break;
322
+ case '>': case '!': order = CA_MV_ORDER_BIG; fmt++; break;
323
+ case '=': case '@': order = host; fmt++; break;
324
+ }
325
+ len = strlen(fmt);
326
+ if ( len == 0 || len >= sizeof(buf) ) return -1;
327
+ memcpy(buf, fmt, len + 1);
328
+
329
+ /* Ruby's suffix form on a single type: "i<", "s!>", "q_<". */
330
+ if ( len >= 2 && len <= 3 && isalpha((unsigned char) buf[0]) &&
331
+ ( buf[len - 1] == '<' || buf[len - 1] == '>' ) ) {
332
+ order = ca_mv_order_merge(order, buf[len - 1] == '<' ? CA_MV_ORDER_LITTLE
333
+ : CA_MV_ORDER_BIG);
334
+ if ( order < 0 ) return -1;
335
+ buf[--len] = '\0';
336
+ }
337
+ /* '_' is Ruby's spelling of the native-size modifier '!'. */
338
+ if ( len == 2 && buf[1] == '_' ) {
339
+ buf[1] = '!';
340
+ }
341
+ /* Types that carry their own byte order. */
342
+ if ( len == 1 ) {
343
+ int own = CA_MV_ORDER_NONE;
344
+ switch (buf[0]) {
345
+ case 'e': case 'E': case 'v': case 'V': own = CA_MV_ORDER_LITTLE; break;
346
+ case 'g': case 'G': case 'n': case 'N': own = CA_MV_ORDER_BIG; break;
347
+ }
348
+ order = ca_mv_order_merge(order, own);
349
+ if ( order < 0 ) return -1;
350
+ }
351
+
352
+ one_byte = ( len == 1 && strchr("cCbB?", buf[0]) != NULL );
353
+ if ( order != CA_MV_ORDER_NONE && order != host && ! one_byte ) {
354
+ return -1;
330
355
  }
331
- if (*fmt == '\0') return -1;
356
+ fmt = buf;
332
357
 
333
358
  /* single-byte formats */
334
359
  if (strcmp(fmt, "c") == 0 && item_size == 1) return CA_INT8;
@@ -341,24 +366,32 @@ ca_mv_data_type_from_format (const char *format, ssize_t item_size)
341
366
  if ((strcmp(fmt, "s") == 0 || strcmp(fmt, "s!") == 0 ||
342
367
  strcmp(fmt, "h") == 0) && item_size == 2) return CA_INT16;
343
368
  if ((strcmp(fmt, "S") == 0 || strcmp(fmt, "S!") == 0 ||
344
- strcmp(fmt, "H") == 0) && item_size == 2) return CA_UINT16;
369
+ strcmp(fmt, "H") == 0 || strcmp(fmt, "v") == 0 ||
370
+ strcmp(fmt, "n") == 0) && item_size == 2) return CA_UINT16;
345
371
 
346
372
  /* 32-bit */
347
373
  if ((strcmp(fmt, "l") == 0 || strcmp(fmt, "i") == 0 ||
348
- strcmp(fmt, "i!") == 0 || strcmp(fmt, "l!") == 0) && item_size == 4) return CA_INT32;
374
+ strcmp(fmt, "i!") == 0 || strcmp(fmt, "l!") == 0 ||
375
+ strcmp(fmt, "j") == 0) && item_size == 4) return CA_INT32;
349
376
  if ((strcmp(fmt, "L") == 0 || strcmp(fmt, "I") == 0 ||
350
- strcmp(fmt, "I!") == 0 || strcmp(fmt, "L!") == 0) && item_size == 4) return CA_UINT32;
377
+ strcmp(fmt, "I!") == 0 || strcmp(fmt, "L!") == 0 ||
378
+ strcmp(fmt, "J") == 0 || strcmp(fmt, "V") == 0 ||
379
+ strcmp(fmt, "N") == 0) && item_size == 4) return CA_UINT32;
351
380
 
352
381
  /* 64-bit. `l`/`L` at item_size 8 accepted as LP64 native long
353
382
  (numpy default for `np.array([1,2,3]).dtype == int64`). */
354
383
  if ((strcmp(fmt, "q") == 0 || strcmp(fmt, "q!") == 0 ||
355
- strcmp(fmt, "l!") == 0 || strcmp(fmt, "l") == 0) && item_size == 8) return CA_INT64;
384
+ strcmp(fmt, "l!") == 0 || strcmp(fmt, "l") == 0 ||
385
+ strcmp(fmt, "j") == 0) && item_size == 8) return CA_INT64;
356
386
  if ((strcmp(fmt, "Q") == 0 || strcmp(fmt, "Q!") == 0 ||
357
- strcmp(fmt, "L!") == 0 || strcmp(fmt, "L") == 0) && item_size == 8) return CA_UINT64;
387
+ strcmp(fmt, "L!") == 0 || strcmp(fmt, "L") == 0 ||
388
+ strcmp(fmt, "J") == 0) && item_size == 8) return CA_UINT64;
358
389
 
359
- /* floats: no endian-bearing variants accepted */
360
- if (strcmp(fmt, "f") == 0 && item_size == 4) return CA_FLOAT32;
361
- if (strcmp(fmt, "d") == 0 && item_size == 8) return CA_FLOAT64;
390
+ /* floats */
391
+ if ((strcmp(fmt, "f") == 0 || strcmp(fmt, "e") == 0 ||
392
+ strcmp(fmt, "g") == 0) && item_size == 4) return CA_FLOAT32;
393
+ if ((strcmp(fmt, "d") == 0 || strcmp(fmt, "E") == 0 ||
394
+ strcmp(fmt, "G") == 0) && item_size == 8) return CA_FLOAT64;
362
395
 
363
396
  /* complex: PEP 3118 'Zf' / 'Zd' canonical. */
364
397
  if (strcmp(fmt, "Zf") == 0 && item_size == 8) return CA_CMPLX64;
@@ -424,35 +457,6 @@ ca_mv_carray_bytes_for (int8_t data_type, ssize_t item_size)
424
457
  : (ca_size_t) ca_sizeof[data_type];
425
458
  }
426
459
 
427
- /* ---------------- strategy lookup ---------------- */
428
-
429
- static ca_mv_strategy_t
430
- ca_mv_strategy_for (int16_t obj_type)
431
- {
432
- /* compile-time enum members */
433
- if (obj_type == CA_OBJ_ARRAY ||
434
- obj_type == CA_OBJ_ARRAY_WRAP ||
435
- obj_type == CA_OBJ_SCALAR) return CA_MV_DIRECT;
436
- if (obj_type == CA_OBJ_REFER) return CA_MV_REFER;
437
- if (obj_type == CA_OBJ_BLOCK) return CA_MV_BLOCK;
438
- if (obj_type == CA_OBJ_SELECT) return CA_MV_ATTACH;
439
- if (obj_type == CA_OBJ_REPEAT) return CA_MV_REPEAT;
440
- if (obj_type == CA_OBJ_OBJECT) return CA_MV_REJECT;
441
- /* Plain CAStride (created via #as_strided, or wrap_memory_view from
442
- a strided producer). Reuse the CA_MV_TRANS handler since it
443
- already reads strides[] / base_offset directly from the CAStride
444
- layout -- no CATranspose-specific code in that path anymore. */
445
- if (obj_type == CA_OBJ_STRIDE) return CA_MV_TRANS;
446
-
447
- /* runtime-assigned obj_types */
448
- for (int i = 0; i < CA_MV_NUM_RUNTIME_OBJ_TYPES; i++) {
449
- if (ca_mv_runtime_types[i].id == obj_type) {
450
- return (ca_mv_strategy_t) ca_mv_runtime_types[i].strategy;
451
- }
452
- }
453
- return CA_MV_REJECT;
454
- }
455
-
456
460
  /* ---------------- helpers ---------------- */
457
461
 
458
462
  static int
@@ -507,6 +511,8 @@ typedef struct {
507
511
  int depth; /* 0 = the view itself, 1 = parent, 2 = grand-, ... */
508
512
  const char *class_name; /* class name of the offending link */
509
513
  const char *reason; /* short explanation */
514
+ int face; /* refused because it is a Face: a copy is
515
+ refused too, so no copy is suggested */
510
516
  } ca_mv_reject_t;
511
517
 
512
518
  static bool ca_mv_stride_is_contig (CAStride *ca); /* defined below */
@@ -551,6 +557,18 @@ ca_mv_reject_reason_for (CArray *ca)
551
557
  return "this kind of view is not alias-capable";
552
558
  }
553
559
 
560
+ /* The entities whose bytes the producer exports: CArray, CAWrap and
561
+ CScalar. An entity installed from outside the core (ca_install_obj_type,
562
+ e.g. a CASource subclass) may hold its bytes elsewhere (cold at rest),
563
+ so neither it nor a view onto it is exported. */
564
+ static inline bool
565
+ ca_mv_is_core_entity (CArray *ca)
566
+ {
567
+ return ca->obj_type == CA_OBJ_ARRAY ||
568
+ ca->obj_type == CA_OBJ_ARRAY_WRAP ||
569
+ ca->obj_type == CA_OBJ_SCALAR;
570
+ }
571
+
554
572
  /* Resolve ca to an (entity, composed_strides, composed_base) triple
555
573
  suitable for zero-copy strided export. Uses ca_stride_compose_to_root
556
574
  to fold the CAStride parent chain (alias-eligible OR not, as long
@@ -569,15 +587,31 @@ ca_mv_compose_to_entity (CArray *ca, ca_mv_reject_t *rej,
569
587
  ca_size_t *out_strides,
570
588
  ca_size_t *out_base)
571
589
  {
572
- /* Face (= CARecord etc.) is a 1:1 alias of parent->ptr, so
573
- storage-layer-wise it is identical to the parent's entity / castride
574
- layer. Strip Face, descend to parent, and feed the subsequent
575
- compose. data_class / format come separately from the Face tail via
576
- rb_ca_data_class universal dispatch. */
590
+ rej->face = 0;
591
+ /* A Face is a 1:1 alias of its storage, so it is exported as that
592
+ storage -- but only a Face that registered its bytes as carrying its
593
+ values (ca_face_register_memory_view). The format comes from the Face
594
+ (data_class for a CARecord); the layout from the storage below. */
577
595
  if (ca_is_face(ca)) {
596
+ if (! ca_face_memory_view_exportable(ca)) {
597
+ rej->depth = 0;
598
+ rej->class_name = ca_mv_class_name_of(ca);
599
+ rej->reason = "a Face whose storage bytes do not carry its values "
600
+ "without the Face; export its storage instead "
601
+ "(e.g. .ticks, .codes)";
602
+ rej->face = 1;
603
+ return false;
604
+ }
578
605
  ca = ca_strip_face(ca);
579
606
  }
580
607
  if (ca_is_entity(ca)) {
608
+ if (! ca_mv_is_core_entity(ca)) {
609
+ rej->depth = 0;
610
+ rej->class_name = ca_mv_class_name_of(ca);
611
+ rej->reason = "installed outside the core; its bytes are not "
612
+ "exported";
613
+ return false;
614
+ }
581
615
  *out_entity = ca;
582
616
  *out_base = 0;
583
617
  return true;
@@ -601,6 +635,12 @@ ca_mv_compose_to_entity (CArray *ca, ca_mv_reject_t *rej,
601
635
  }
602
636
  return false;
603
637
  }
638
+ if (! ca_mv_is_core_entity(root)) {
639
+ rej->depth = -1;
640
+ rej->class_name = ca_mv_class_name_of(root);
641
+ rej->reason = "installed outside the core; its bytes are not exported";
642
+ return false;
643
+ }
604
644
  *out_entity = root;
605
645
  return true;
606
646
  }
@@ -641,30 +681,57 @@ ca_mv_available_p (VALUE obj)
641
681
  if (! ca_mv_extract(obj, &ca)) return false;
642
682
  if (! ca_mv_ca_exportable(ca, obj)) return false;
643
683
  if (ca_has_mask(ca)) return false;
644
- /* An entity whose obj_type has no strategy of its own is one installed
645
- from outside the core (ca_install_obj_type). It reaches this callback
646
- only because it inherits CArray's MemoryView registration, and
647
- ca_mv_get would then reject it — say so here rather than advertising
648
- an export that cannot be produced. Built-in entities (CArray /
649
- CAWrap / CScalar) map to DIRECT and are unaffected. */
650
- if (ca_is_entity(ca) && ca_mv_strategy_for(ca->obj_type) == CA_MV_REJECT) {
651
- return false;
652
- }
653
- /* Wrap-via-MV exports are zero-copy: require an alias chain to
654
- entity. Non-alias views (CASelect, CAFake, etc.) and CAStride
684
+ /* The same resolver decides the export in ca_mv_get, so what this
685
+ says is what the export does. Wrap-via-MV exports are zero-copy:
686
+ require an alias chain to a core entity. Non-alias views (CASelect, CAFake, etc.) and CAStride
655
687
  chains broken by a non-contig link cannot be wrapped without a
656
688
  snapshot, which has wrong semantics for MV consumers. */
657
689
  return ca_mv_check_alias_chain(ca, &rej);
658
690
  }
659
691
 
692
+ /* "Ns" format for a plain CA_FIXLEN of N bytes. The C strings are kept
693
+ in a table keyed by N for the life of the process, so the pointer stays
694
+ valid for any view, and a frozen array (which cannot take an ivar) is
695
+ exported like any other. The table is shared by every Ractor, hence
696
+ the lock. */
697
+ static st_table *ca_mv_fixlen_formats = NULL;
698
+ static rb_nativethread_lock_t ca_mv_fixlen_formats_lock;
699
+
700
+ static const char *
701
+ ca_mv_fixlen_format (ca_size_t bytes)
702
+ {
703
+ st_data_t found;
704
+ char *fmt;
705
+ rb_nativethread_lock_lock(&ca_mv_fixlen_formats_lock);
706
+ if ( st_lookup(ca_mv_fixlen_formats, (st_data_t) bytes, &found) ) {
707
+ rb_nativethread_lock_unlock(&ca_mv_fixlen_formats_lock);
708
+ return (const char *) found;
709
+ }
710
+ rb_nativethread_lock_unlock(&ca_mv_fixlen_formats_lock);
711
+ fmt = ALLOC_N(char, 24);
712
+ snprintf(fmt, 24, "%llds", (long long) bytes);
713
+ rb_nativethread_lock_lock(&ca_mv_fixlen_formats_lock);
714
+ if ( st_lookup(ca_mv_fixlen_formats, (st_data_t) bytes, &found) ) {
715
+ xfree(fmt);
716
+ fmt = (char *) found;
717
+ }
718
+ else {
719
+ st_insert(ca_mv_fixlen_formats, (st_data_t) bytes, (st_data_t) fmt);
720
+ }
721
+ rb_nativethread_lock_unlock(&ca_mv_fixlen_formats_lock);
722
+ return fmt;
723
+ }
724
+
660
725
  static bool
661
726
  ca_mv_get (VALUE obj, rb_memory_view_t *view, int flags)
662
727
  {
663
728
  CArray *ca;
664
- ca_mv_strategy_t strategy;
729
+ CArray *storage;
665
730
  ssize_t *shape = NULL;
666
731
  ssize_t *strides = NULL;
667
732
  void *data_ptr = NULL;
733
+ const char *format;
734
+ char *record_format = NULL;
668
735
  int writable_request;
669
736
 
670
737
  if (! ca_mv_extract(obj, &ca)) return false;
@@ -678,11 +745,10 @@ ca_mv_get (VALUE obj, rb_memory_view_t *view, int flags)
678
745
  Object#freeze on a subclass). */
679
746
  if (writable_request && OBJ_FROZEN(obj)) return false;
680
747
 
681
- /* Resolve the chain to (entity, composed_strides, composed_base).
682
- If ca isn't a CAStride family member, only entity-class is
683
- accepted here. The CAStride-family case below uses composed_strides
684
- / composed_base directly; the DIRECT case ignores them (entity owns
685
- ca->ptr already, no compose needed). */
748
+ /* Resolve the chain to (entity, composed_strides, composed_base) with
749
+ the resolver available_p uses, so the two cannot disagree. The
750
+ layout is the storage's: a Face that got through is a 1:1 alias of
751
+ it. */
686
752
  CArray *resolved_entity = NULL;
687
753
  ca_size_t composed_strides[CA_RANK_MAX];
688
754
  ca_size_t composed_base = 0;
@@ -693,73 +759,72 @@ ca_mv_get (VALUE obj, rb_memory_view_t *view, int flags)
693
759
  return false;
694
760
  }
695
761
  }
762
+ storage = ca_is_face(ca) ? ca_strip_face(ca) : ca;
696
763
 
697
764
  /* SIMPLE consumer on non-contig view would need a snapshot -- not
698
765
  offered (consumers wanting a snapshot use from_memory_view). */
699
766
  {
700
767
  int wants_strides_early = (flags & RUBY_MEMORY_VIEW_STRIDES) ? 1 : 0;
701
768
  if (!wants_strides_early &&
702
- ca_mv_is_castride_family(ca) &&
703
- !ca_mv_stride_is_contig((CAStride *) ca)) {
769
+ ca_mv_is_castride_family(storage) &&
770
+ !ca_mv_stride_is_contig((CAStride *) storage)) {
704
771
  return false;
705
772
  }
706
773
  }
707
774
 
708
- strategy = ca_mv_strategy_for(ca->obj_type);
775
+ /* Format string, resolved before shape / strides are allocated since
776
+ building it can raise. Primitive types use the top-level table;
777
+ CA_FIXLEN + data_class emits the record's format, cached on the class
778
+ as a String. The String can move, so the view carries its own copy in
779
+ private_data, freed by ca_mv_release. */
780
+ if (ca->data_type == CA_FIXLEN && RTEST(rb_ca_has_data_class(obj))) {
781
+ VALUE klass = rb_ca_data_class(obj);
782
+ VALUE fmt = ca_mv_struct_format_for_data_class(klass);
783
+ long len = RSTRING_LEN(fmt);
784
+ record_format = ALLOC_N(char, len + 1);
785
+ memcpy(record_format, RSTRING_PTR(fmt), len);
786
+ record_format[len] = '\0';
787
+ RB_GC_GUARD(fmt);
788
+ format = record_format;
789
+ }
790
+ else if (ca->data_type == CA_FIXLEN) {
791
+ /* Plain CA_FIXLEN (no data_class): PEP 3118 "Ns" fixed-bytes. */
792
+ format = ca_mv_fixlen_format(ca->bytes);
793
+ }
794
+ else {
795
+ format = ca_mv_format_for(ca->data_type);
796
+ }
709
797
 
710
- switch (strategy) {
711
- case CA_MV_DIRECT:
712
- /* Entity / wrap / scalar: ca->ptr is the entity's own buffer, always
713
- valid. No attach needed. */
714
- data_ptr = ca->ptr;
715
- if (ca->ndim >= 1) {
798
+ if (ca_is_entity(storage)) {
799
+ /* Entity / wrap / scalar: its own buffer, always valid. No attach
800
+ needed. A CScalar is exported with ndim 0 and no shape, so none is
801
+ taken: release frees only what the view carries. */
802
+ data_ptr = storage->ptr;
803
+ if (ca->ndim >= 1 && storage->obj_type != CA_OBJ_SCALAR) {
716
804
  shape = ca_mv_alloc_shape(ca);
717
805
  strides = ca_mv_alloc_strides_contiguous(ca, shape, true);
718
806
  }
719
- break;
720
-
721
- /* All CAStride-family views (CARefer / CABlock / CAFarray / CATranspose
722
- / CARepeat / plain CAStride) reach this point only after
723
- ca_mv_compose_to_entity succeeded -- the producer is alias-only by
724
- ca_mv_available_p contract. Expose `entity->ptr + composed_base` as
725
- data_ptr and composed_strides as strides, both in entity byte space.
726
- The entity owns its bytes (always valid via view->obj's parent chain
727
- while the consumer holds the view).
728
-
729
- CAREFUL: do not ca_attach the view here (nor ca_sync / ca_detach at
730
- release). Exposure is alias-only: the consumer writes directly into
731
- root bytes, so a sync would scatter bytes back to root as a no-op
732
- chain -- and that chain triggers ca_update_mask / ca_sync(mask)
733
- recursion that can raise on an invariant violation. Raising from a
734
- release callback is fatal during GC sweep (newobj_of during sweep ->
735
- BUG). */
736
- case CA_MV_REFER:
737
- case CA_MV_BLOCK:
738
- case CA_MV_FARRAY:
739
- case CA_MV_TRANS:
740
- case CA_MV_REPEAT: {
807
+ }
808
+ else {
809
+ /* A CAStride-family view (CARefer / CABlock / CAFarray / CATranspose
810
+ / CARepeat / CAField / plain CAStride) composed to an entity.
811
+ Expose `entity->ptr + composed_base` as data_ptr and
812
+ composed_strides as strides, both in entity byte space. The entity
813
+ owns its bytes (always valid via view->obj's parent chain while the
814
+ consumer holds the view).
815
+
816
+ CAREFUL: do not ca_attach the view here (nor ca_sync / ca_detach at
817
+ release). Exposure is alias-only: the consumer writes directly into
818
+ root bytes, so a sync would scatter bytes back to root as a no-op
819
+ chain -- and that chain triggers ca_update_mask / ca_sync(mask)
820
+ recursion that can raise on an invariant violation. Raising from a
821
+ release callback is fatal during GC sweep (newobj_of during sweep ->
822
+ BUG). */
741
823
  int8_t k;
742
824
  shape = ca_mv_alloc_shape(ca);
743
825
  strides = (ssize_t *) xmalloc(sizeof(ssize_t) * ca->ndim);
744
826
  for (k = 0; k < ca->ndim; k++) strides[k] = (ssize_t) composed_strides[k];
745
827
  data_ptr = resolved_entity->ptr + composed_base;
746
- break;
747
- }
748
-
749
- case CA_MV_ATTACH:
750
- /* Should be unreachable: ca_mv_check_alias_chain rejects every
751
- obj_type that maps to ATTACH (CASelect / CAMapping / CAGrid /
752
- CAShift / CAWindow / CAFake / CAReduce). Materialise is no
753
- longer offered via wrap_memory_view; from_memory_view gives a
754
- snapshot copy instead. */
755
- rb_raise(rb_eRuntimeError,
756
- "[BUG] ATTACH strategy reached in ca_mv_get for %s; "
757
- "alias-chain check should have rejected it",
758
- ca_mv_class_name_of(ca));
759
-
760
- case CA_MV_REJECT:
761
- default:
762
- return false;
763
828
  }
764
829
 
765
830
  view->obj = obj;
@@ -770,41 +835,15 @@ ca_mv_get (VALUE obj, rb_memory_view_t *view, int flags)
770
835
  views compute Σ (shape[k]-1)*|strides[k]| + item_size themselves. */
771
836
  view->byte_size = (ssize_t) ca_length(ca);
772
837
  view->readonly = ca_is_readonly(ca) ? true : false;
773
- /* Format string: primitive types use the top-level PEP 3118 table;
774
- CA_FIXLEN + data_class emits a PEP 3118 "T{...}" struct format
775
- whose lifetime is bound to the class via @__mv_struct_format__,
776
- so the view->format pointer is stable until the class is GC'd. */
777
- if (ca->data_type == CA_FIXLEN && RTEST(rb_ca_has_data_class(obj))) {
778
- VALUE klass = rb_ca_data_class(obj);
779
- VALUE fmt = ca_mv_struct_format_for_data_class(klass);
780
- view->format = RSTRING_PTR(fmt);
781
- }
782
- else if (ca->data_type == CA_FIXLEN) {
783
- /* Plain CA_FIXLEN (no data_class): emit PEP 3118 "Ns" fixed-bytes.
784
- Cache the format String on the source object so RSTRING_PTR stays
785
- stable for the view's lifetime; the source object is anchored as
786
- view->obj. */
787
- VALUE fmt_str = rb_attr_get(obj, rb_intern("__mv_fixlen_format__"));
788
- if (NIL_P(fmt_str)) {
789
- char fmt_buf[32];
790
- snprintf(fmt_buf, sizeof(fmt_buf), "%lds", (long) ca->bytes);
791
- fmt_str = rb_str_new_cstr(fmt_buf);
792
- rb_obj_freeze(fmt_str);
793
- rb_ivar_set(obj, rb_intern("__mv_fixlen_format__"), fmt_str);
794
- }
795
- view->format = RSTRING_PTR(fmt_str);
796
- }
797
- else {
798
- view->format = ca_mv_format_for(ca->data_type);
799
- }
838
+ view->format = format;
800
839
  view->item_size = (ssize_t) ca->bytes;
801
840
  view->item_desc.components = NULL;
802
841
  view->item_desc.length = 0;
803
- view->ndim = (ca->obj_type == CA_OBJ_SCALAR) ? 0 : (ssize_t) ca->ndim;
842
+ view->ndim = (storage->obj_type == CA_OBJ_SCALAR) ? 0 : (ssize_t) ca->ndim;
804
843
  view->shape = (view->ndim == 0) ? NULL : shape;
805
844
  view->strides = (view->ndim == 0) ? NULL : strides;
806
845
  view->sub_offsets = NULL;
807
- view->private_data = NULL;
846
+ view->private_data = record_format;
808
847
 
809
848
  /* Enforce caller's layout request. */
810
849
  {
@@ -827,6 +866,11 @@ reject:
827
866
  return false; there is no attach state to unwind. */
828
867
  if (shape) { xfree(shape); view->shape = NULL; }
829
868
  if (strides) { xfree(strides); view->strides = NULL; }
869
+ if (record_format) {
870
+ xfree(record_format);
871
+ view->format = NULL;
872
+ view->private_data = NULL;
873
+ }
830
874
  return false;
831
875
  }
832
876
 
@@ -848,7 +892,10 @@ ca_mv_release (VALUE obj, rb_memory_view_t *view)
848
892
  ca_sync(ca->mask) recursion and raise; raising from a release
849
893
  callback is fatal during GC sweep. See ca_mv_get's CAStride-family
850
894
  case comment for the full rationale. */
851
- view->private_data = NULL;
895
+ if (view->private_data) {
896
+ xfree(view->private_data);
897
+ view->private_data = NULL;
898
+ }
852
899
  return true;
853
900
  }
854
901
 
@@ -867,9 +914,25 @@ ca_mv_release (VALUE obj, rb_memory_view_t *view)
867
914
  a TypedData holder (also stored as an ivar) releases the view in its
868
915
  dfree callback when the CAWrap is collected. */
869
916
 
917
+ /* One borrowed view and the geometry read from it. The geometry is
918
+ copied out and normalised once (a 0-d view as one element, omitted
919
+ shape or strides filled in as row-major), so nothing below reads the
920
+ producer's shape or strides arrays again.
921
+
922
+ The holder is a Ruby object from before the view is taken, so a raise
923
+ at any later point loses nothing: the importers release it at once
924
+ (rb_protect / rb_ensure), and the holder's dfree releases it in any
925
+ case. `lo` / `hi` bound the bytes the elements reach, relative to
926
+ view.data. */
870
927
  typedef struct {
871
928
  rb_memory_view_t view;
872
929
  bool valid;
930
+ int8_t ndim;
931
+ ca_size_t dim[CA_RANK_MAX];
932
+ ssize_t strides[CA_RANK_MAX];
933
+ ca_size_t elements;
934
+ bool contiguous;
935
+ ssize_t lo, hi;
873
936
  } ca_mv_imported_holder_t;
874
937
 
875
938
  static void
@@ -877,8 +940,21 @@ ca_mv_imported_holder_free (void *p)
877
940
  {
878
941
  ca_mv_imported_holder_t *h = (ca_mv_imported_holder_t *) p;
879
942
  if (h->valid) {
880
- rb_memory_view_release(&h->view);
881
943
  h->valid = false;
944
+ /* While a view is exported Ruby keeps the producer alive, so in an
945
+ ordinary sweep it is still here. At exit, though, the objects
946
+ left are freed in heap order and the producer may have gone first
947
+ (its slot is T_NONE with the class still set). Its release
948
+ callback would then get a dead object, and one that checks the
949
+ type raises, which is fatal during GC. Skip the release there:
950
+ the process is ending, so only the components are freed. */
951
+ if (! RB_SPECIAL_CONST_P(h->view.obj) &&
952
+ RB_BUILTIN_TYPE(h->view.obj) == RUBY_T_NONE) {
953
+ xfree((void *) h->view.item_desc.components);
954
+ }
955
+ else {
956
+ rb_memory_view_release(&h->view);
957
+ }
882
958
  }
883
959
  xfree(h);
884
960
  }
@@ -900,149 +976,311 @@ static const rb_data_type_t ca_mv_imported_holder_data_type = {
900
976
  .flags = RUBY_TYPED_FREE_IMMEDIATELY
901
977
  };
902
978
 
903
- /* Acquire a MemoryView from src and validate/derive data_type, ndim
904
- and shape. Tries STRIDES first; falls back to SIMPLE so that
905
- producers that advertise only contiguous still work. On any
906
- failure, releases the view (if acquired) and raises ArgumentError;
907
- on success, the view is left active and the caller owns the
908
- responsibility to release it.
909
-
910
- `explicit_data_type` is the caller-supplied target data_type (>= 0) or -1
911
- if the caller has no preference. Behaviour:
912
- - explicit_data_type < 0, format != NULL: derive data_type from format.
913
- - explicit_data_type < 0, format == NULL: reject (typeless source
914
- without consumer-supplied type cannot be interpreted).
915
- - explicit_data_type >= 0, format != NULL: must match the format's
916
- data_type; mismatch -> reject.
917
- - explicit_data_type >= 0, format == NULL: use explicit_data_type; the
918
- byte buffer is reinterpreted as that data_type. ndim is forced
919
- to 1 (multi-dim from a typeless source is ambiguous; user
920
- chains .reshape).
921
- */
922
- /* Row-major contiguity test that tolerates a NULL shape array. ndim-0
923
- scalars and SIMPLE-form producers (which omit the shape array for a
924
- contiguous 1-D buffer, e.g. red-arrow's primitive arrays) are
925
- contiguous by construction; rb_memory_view_is_row_major_contiguous
926
- dereferences view->shape, so it must not be reached with shape NULL. */
927
- static bool
928
- ca_mv_view_is_contiguous (const rb_memory_view_t *view)
979
+ static VALUE
980
+ ca_mv_holder_new (ca_mv_imported_holder_t **out)
929
981
  {
930
- if (view->ndim == 0 || view->shape == NULL) {
931
- return true;
982
+ /* Zero-filled: valid is false and the view's item_desc.components is
983
+ NULL, which release frees unconditionally (red-arrow 24.0.0 leaves
984
+ that field unwritten, apache/arrow#45187). */
985
+ return TypedData_Make_Struct(rb_cObject, ca_mv_imported_holder_t,
986
+ &ca_mv_imported_holder_data_type, *out);
987
+ }
988
+
989
+ static void
990
+ ca_mv_holder_release (ca_mv_imported_holder_t *h)
991
+ {
992
+ if (h != NULL && h->valid) {
993
+ h->valid = false;
994
+ rb_memory_view_release(&h->view);
932
995
  }
933
- return rb_memory_view_is_row_major_contiguous(view);
934
996
  }
935
997
 
998
+ /* Take a view from src: STRIDES first, SIMPLE for producers that offer
999
+ only a contiguous buffer. `writable` asks the producer for a writable
1000
+ view; a producer that refuses that but gives a read-only one is a
1001
+ read-only source. */
936
1002
  static void
937
- ca_mv_acquire_and_validate (VALUE src, rb_memory_view_t *view,
938
- int8_t explicit_data_type,
939
- int8_t *out_data_type, int8_t *out_ndim,
940
- ca_size_t *out_dim)
1003
+ ca_mv_holder_acquire (ca_mv_imported_holder_t *h, VALUE src, int writable,
1004
+ const char *who)
941
1005
  {
942
- int8_t data_type;
943
- int i;
1006
+ int extra = writable ? RUBY_MEMORY_VIEW_WRITABLE : 0;
1007
+ memset(&h->view, 0, sizeof(h->view));
1008
+ if ( ! rb_memory_view_get(src, &h->view, RUBY_MEMORY_VIEW_STRIDES | extra) ) {
1009
+ memset(&h->view, 0, sizeof(h->view));
1010
+ if ( ! rb_memory_view_get(src, &h->view, RUBY_MEMORY_VIEW_SIMPLE | extra) ) {
1011
+ if ( writable ) {
1012
+ memset(&h->view, 0, sizeof(h->view));
1013
+ if ( rb_memory_view_get(src, &h->view, RUBY_MEMORY_VIEW_STRIDES) ||
1014
+ rb_memory_view_get(src, &h->view, RUBY_MEMORY_VIEW_SIMPLE) ) {
1015
+ h->valid = true;
1016
+ rb_raise(rb_eRuntimeError,
1017
+ "MemoryView source is read-only; can't wrap as writable");
1018
+ }
1019
+ }
1020
+ rb_raise(rb_eArgError, "%sobject does not support MemoryView", who);
1021
+ }
1022
+ }
1023
+ h->valid = true;
1024
+ if ( writable && h->view.readonly ) {
1025
+ rb_raise(rb_eRuntimeError,
1026
+ "MemoryView source is read-only; can't wrap as writable");
1027
+ }
1028
+ }
944
1029
 
945
- /* Defensive zero-init: rb_memory_view_release() unconditionally
946
- xfree()s view->item_desc.components, but not every producer writes
947
- that field. red-arrow 24.0.0 leaves it uninitialized, so a view
948
- acquired from it frees stack garbage on release (abort). Zeroing
949
- here makes the field NULL. Consumer side of apache/arrow#45187
950
- (fixed in Arrow 25.0.0); harmless once producers initialize it. */
951
- memset(view, 0, sizeof(*view));
1030
+ /* Read the view's geometry for elements of `item_size` bytes, and check
1031
+ it against the buffer. The buffer is [data, data + byte_size). For a
1032
+ strided view byte_size is the element count times item_size (the
1033
+ convention CArray's own producer, NumPy and Arrow follow), which does
1034
+ not bound where the strides reach, so only that count is checked; for
1035
+ a contiguous view the shape is checked against the bytes. */
1036
+ static void
1037
+ ca_mv_holder_read_geometry (ca_mv_imported_holder_t *h, ssize_t item_size,
1038
+ const char *who)
1039
+ {
1040
+ const rb_memory_view_t *v = &h->view;
1041
+ ssize_t expect, need;
1042
+ int8_t k;
952
1043
 
953
- if (! rb_memory_view_get(src, view, RUBY_MEMORY_VIEW_STRIDES)) {
954
- if (! rb_memory_view_get(src, view, RUBY_MEMORY_VIEW_SIMPLE)) {
1044
+ /* rank-check: a producer's rank, named by which MemoryView it is */
1045
+ if ( v->ndim < 0 || v->ndim > CA_RANK_MAX ) {
1046
+ rb_raise(rb_eArgError, "%sMemoryView ndim (%ld) exceeds CArray CA_RANK_MAX (%d)",
1047
+ who, (long) v->ndim, CA_RANK_MAX);
1048
+ }
1049
+ if ( v->ndim == 0 ) {
1050
+ h->ndim = 1;
1051
+ h->dim[0] = 1;
1052
+ }
1053
+ else if ( v->shape == NULL ) {
1054
+ /* Only 1-D is readable without a shape: length = byte_size / item_size. */
1055
+ if ( v->ndim > 1 ) {
955
1056
  rb_raise(rb_eArgError,
956
- "object does not support MemoryView");
1057
+ "%sMemoryView reports ndim %ld but provides no shape array",
1058
+ who, (long) v->ndim);
1059
+ }
1060
+ h->ndim = 1;
1061
+ h->dim[0] = (ca_size_t) (item_size > 0 ? v->byte_size / item_size : 0);
1062
+ }
1063
+ else {
1064
+ h->ndim = (int8_t) v->ndim;
1065
+ for (k = 0; k < h->ndim; k++) {
1066
+ if ( v->shape[k] < 0 ) {
1067
+ rb_raise(rb_eArgError, "%sMemoryView shape[%d] is negative (%ld)",
1068
+ who, (int) k, (long) v->shape[k]);
1069
+ }
1070
+ h->dim[k] = (ca_size_t) v->shape[k];
957
1071
  }
958
1072
  }
1073
+ h->elements = ca_shape_elements(h->ndim, h->dim, (ca_size_t) item_size);
959
1074
 
960
- if (view->format == NULL) {
961
- /* Typeless producer (e.g. mmap-view, IO#read-style byte blob).
962
- Consumer must supply the target data_type. */
963
- if (explicit_data_type < 0) {
964
- ssize_t got = view->byte_size;
965
- rb_memory_view_release(view);
1075
+ if ( v->ndim >= 1 && v->shape != NULL && v->strides != NULL ) {
1076
+ for (k = 0; k < h->ndim; k++) {
1077
+ h->strides[k] = v->strides[k];
1078
+ }
1079
+ }
1080
+ else {
1081
+ expect = item_size;
1082
+ for (k = h->ndim - 1; k >= 0; k--) {
1083
+ h->strides[k] = expect;
1084
+ expect *= (ssize_t) h->dim[k];
1085
+ }
1086
+ }
1087
+
1088
+ h->contiguous = true;
1089
+ expect = item_size;
1090
+ for (k = h->ndim - 1; k >= 0; k--) {
1091
+ if ( h->dim[k] > 1 && h->strides[k] != expect ) {
1092
+ h->contiguous = false;
1093
+ }
1094
+ expect *= (ssize_t) h->dim[k];
1095
+ }
1096
+
1097
+ h->lo = h->hi = 0;
1098
+ if ( h->elements > 0 ) {
1099
+ h->hi = item_size;
1100
+ for (k = 0; k < h->ndim; k++) {
1101
+ ssize_t reach;
1102
+ if ( __builtin_mul_overflow((ssize_t) h->dim[k] - 1, h->strides[k], &reach) ||
1103
+ ( reach < 0 ? __builtin_add_overflow(h->lo, reach, &h->lo)
1104
+ : __builtin_add_overflow(h->hi, reach, &h->hi) ) ) {
1105
+ rb_raise(rb_eArgError, "%sMemoryView strides reach too far", who);
1106
+ }
1107
+ }
1108
+ }
1109
+
1110
+ need = (ssize_t) h->elements * item_size;
1111
+ if ( v->byte_size < need ) {
1112
+ rb_raise(rb_eArgError,
1113
+ "%sMemoryView reports %ld bytes, but its shape needs %ld",
1114
+ who, (long) v->byte_size, (long) need);
1115
+ }
1116
+ }
1117
+
1118
+ /* Take the data side's view and resolve its data_type.
1119
+ `explicit_data_type` (the data_type: keyword) must agree with a typed
1120
+ producer's format and names the type of a typeless one.
1121
+ `typeless_data_type` names the type only when the producer is typeless
1122
+ (wrap_readonly / wrap_writable, which adapt a typed producer
1123
+ afterwards). A typeless producer's bytes are read as one 1-D run, so
1124
+ one whose own geometry is strided is refused. */
1125
+ static int8_t
1126
+ ca_mv_holder_take_data (ca_mv_imported_holder_t *h, VALUE src,
1127
+ int8_t explicit_data_type, int8_t typeless_data_type,
1128
+ int writable)
1129
+ {
1130
+ rb_memory_view_t *v = &h->view;
1131
+ int8_t data_type;
1132
+
1133
+ ca_mv_holder_acquire(h, src, writable, "");
1134
+
1135
+ if ( v->format == NULL ) {
1136
+ ssize_t target;
1137
+ data_type = ( explicit_data_type >= 0 ) ? explicit_data_type : typeless_data_type;
1138
+ if ( data_type < 0 ) {
966
1139
  rb_raise(rb_eArgError,
967
1140
  "typeless MemoryView (format=NULL, %ld bytes); "
968
1141
  "specify the target data_type via data_type: kwarg or use a "
969
1142
  "concrete CArray::<Dtype> factory class",
970
- (long) got);
1143
+ (long) v->byte_size);
971
1144
  }
972
- if (ca_mv_format_for(explicit_data_type) == NULL) {
973
- rb_memory_view_release(view);
1145
+ if ( ca_mv_format_for(data_type) == NULL ) {
974
1146
  rb_raise(rb_eArgError,
975
1147
  "target data_type is not representable via MemoryView");
976
1148
  }
977
- ssize_t target_item_size = (ssize_t) ca_sizeof[explicit_data_type];
978
- if (view->byte_size % target_item_size != 0) {
979
- ssize_t bs = view->byte_size;
980
- rb_memory_view_release(view);
1149
+ ca_mv_holder_read_geometry(h, v->item_size > 0 ? v->item_size : 1, "");
1150
+ if ( ! h->contiguous ) {
1151
+ rb_raise(rb_eArgError,
1152
+ "typeless MemoryView is strided; its bytes cannot be read as "
1153
+ "one run of %s", ca_type_name[data_type]);
1154
+ }
1155
+ target = (ssize_t) ca_sizeof[data_type];
1156
+ if ( v->byte_size % target != 0 ) {
981
1157
  rb_raise(rb_eArgError,
982
1158
  "byte_size %ld is not a multiple of target item_size %ld",
983
- (long) bs, (long) target_item_size);
1159
+ (long) v->byte_size, (long) target);
984
1160
  }
985
- data_type = explicit_data_type;
986
- *out_ndim = 1;
987
- out_dim[0] = (ca_size_t) (view->byte_size / target_item_size);
988
- *out_data_type = data_type;
989
- return;
990
- }
991
-
992
- /* Typed producer: derive data_type from format. */
993
- data_type = ca_mv_data_type_from_format(view->format, view->item_size);
994
- if (data_type < 0) {
995
- char fmt_buf[64];
996
- snprintf(fmt_buf, sizeof(fmt_buf), "%s", view->format);
997
- rb_memory_view_release(view);
998
- rb_raise(rb_eArgError,
999
- "unsupported MemoryView format: %s (item_size=%ld)",
1000
- fmt_buf, (long) view->item_size);
1001
- }
1002
-
1003
- /* If the caller specified a data_type too, it must match. */
1004
- if (explicit_data_type >= 0 && explicit_data_type != data_type) {
1005
- rb_memory_view_release(view);
1161
+ h->ndim = 1;
1162
+ h->dim[0] = (ca_size_t) (v->byte_size / target);
1163
+ h->strides[0] = target;
1164
+ h->elements = h->dim[0];
1165
+ h->contiguous = true;
1166
+ h->lo = 0;
1167
+ h->hi = v->byte_size;
1168
+ return data_type;
1169
+ }
1170
+
1171
+ data_type = ca_mv_data_type_from_format(v->format, v->item_size);
1172
+ if ( data_type < 0 ) {
1173
+ rb_raise(rb_eArgError, "unsupported MemoryView format: %s (item_size=%ld)",
1174
+ v->format, (long) v->item_size);
1175
+ }
1176
+ if ( explicit_data_type >= 0 && explicit_data_type != data_type ) {
1006
1177
  rb_raise(rb_eArgError,
1007
1178
  "explicit data_type does not match producer format "
1008
- "(producer: %s, requested data_type %d)",
1009
- view->format, (int) explicit_data_type);
1179
+ "(producer: %s, requested data_type %s)",
1180
+ v->format, ca_type_name[explicit_data_type]);
1010
1181
  }
1182
+ ca_mv_holder_read_geometry(h, v->item_size, "");
1183
+ return data_type;
1184
+ }
1185
+
1186
+ /* Take the mask's view: one byte per element (bool / uint8 / int8),
1187
+ contiguous, of the data side's shape. */
1188
+ static void
1189
+ ca_mv_holder_take_mask (ca_mv_imported_holder_t *m, VALUE src,
1190
+ const ca_mv_imported_holder_t *d)
1191
+ {
1192
+ rb_memory_view_t *v = &m->view;
1193
+ int8_t k;
1011
1194
 
1012
- if (view->ndim == 0) {
1013
- *out_ndim = 1;
1014
- out_dim[0] = 1;
1195
+ ca_mv_holder_acquire(m, src, 0, "mask: ");
1196
+ if ( v->format != NULL ) {
1197
+ int8_t dt = ca_mv_data_type_from_format(v->format, v->item_size);
1198
+ if ( dt != CA_BOOLEAN && dt != CA_UINT8 && dt != CA_INT8 ) {
1199
+ rb_raise(rb_eArgError, "mask: format must be bool/uint8/int8 (got %s)",
1200
+ v->format);
1201
+ }
1015
1202
  }
1016
- else if (view->ndim > CA_RANK_MAX) {
1017
- long got = (long) view->ndim;
1018
- rb_memory_view_release(view);
1019
- rb_raise(rb_eArgError,
1020
- "MemoryView ndim (%ld) exceeds CArray CA_RANK_MAX (%d)",
1021
- got, CA_RANK_MAX);
1022
- }
1023
- else if (view->shape == NULL) {
1024
- /* SIMPLE form: a typed producer may advertise a format but omit the
1025
- shape array for a contiguous 1-D buffer (red-arrow's primitive
1026
- arrays do this). Only 1-D is interpretable without a shape --
1027
- length = byte_size / item_size. A missing shape at ndim > 1 is
1028
- unrecoverable. */
1029
- if (view->ndim > 1) {
1030
- long got = (long) view->ndim;
1031
- rb_memory_view_release(view);
1032
- rb_raise(rb_eArgError,
1033
- "MemoryView reports ndim %ld but provides no shape array",
1034
- got);
1203
+ if ( v->item_size != 1 ) {
1204
+ rb_raise(rb_eArgError, "mask: item_size must be 1 (got %ld)",
1205
+ (long) v->item_size);
1206
+ }
1207
+ ca_mv_holder_read_geometry(m, 1, "mask: ");
1208
+ if ( m->ndim != d->ndim ) {
1209
+ rb_raise(rb_eArgError, "mask: ndim (%d) does not match data ndim (%d)",
1210
+ (int) m->ndim, (int) d->ndim);
1211
+ }
1212
+ for (k = 0; k < d->ndim; k++) {
1213
+ if ( m->dim[k] != d->dim[k] ) {
1214
+ rb_raise(rb_eArgError, "mask: shape[%d] (%ld) does not match data shape (%ld)",
1215
+ (int) k, (long) m->dim[k], (long) d->dim[k]);
1035
1216
  }
1036
- *out_ndim = 1;
1037
- out_dim[0] = (ca_size_t) (view->item_size ? view->byte_size / view->item_size : 0);
1038
1217
  }
1039
- else {
1040
- *out_ndim = (int8_t) view->ndim;
1041
- for (i = 0; i < *out_ndim; i++) {
1042
- out_dim[i] = (ca_size_t) view->shape[i];
1218
+ if ( ! m->contiguous ) {
1219
+ rb_raise(rb_eArgError,
1220
+ "mask: strided masks are not supported in this release; "
1221
+ "pass a row-major contiguous mask buffer");
1222
+ }
1223
+ }
1224
+
1225
+ /* Walk the elements of a view in row-major order, calling `fn` with each
1226
+ element's address. Returns false as soon as fn does. */
1227
+ static bool
1228
+ ca_mv_holder_each (const ca_mv_imported_holder_t *h,
1229
+ bool (*fn)(const char *elem, void *arg), void *arg)
1230
+ {
1231
+ ca_size_t idx[CA_RANK_MAX];
1232
+ ca_size_t n;
1233
+ int8_t k;
1234
+ const char *base = (const char *) h->view.data;
1235
+
1236
+ for (k = 0; k < h->ndim; k++) idx[k] = 0;
1237
+ for (n = 0; n < h->elements; n++) {
1238
+ ssize_t off = 0;
1239
+ for (k = 0; k < h->ndim; k++) {
1240
+ off += (ssize_t) idx[k] * h->strides[k];
1043
1241
  }
1242
+ if ( ! fn(base + off, arg) ) {
1243
+ return false;
1244
+ }
1245
+ for (k = h->ndim - 1; k >= 0; k--) {
1246
+ if ( ++idx[k] < h->dim[k] ) break;
1247
+ idx[k] = 0;
1248
+ }
1249
+ }
1250
+ return true;
1251
+ }
1252
+
1253
+ static bool
1254
+ ca_mv_byte_is_bit (const char *elem, void *arg)
1255
+ {
1256
+ (void) arg;
1257
+ return *(const uint8_t *) elem <= 1;
1258
+ }
1259
+
1260
+ typedef struct {
1261
+ char *dst;
1262
+ size_t item_size;
1263
+ } ca_mv_gather_t;
1264
+
1265
+ static bool
1266
+ ca_mv_gather_one (const char *elem, void *arg)
1267
+ {
1268
+ ca_mv_gather_t *g = (ca_mv_gather_t *) arg;
1269
+ memcpy(g->dst, elem, g->item_size);
1270
+ g->dst += g->item_size;
1271
+ return true;
1272
+ }
1273
+
1274
+ /* A boolean CArray holds 0 or 1 in each byte. A copy can normalise other
1275
+ bytes; a wrap shares them, so it refuses a buffer that holds any. */
1276
+ static void
1277
+ ca_mv_check_bits (const ca_mv_imported_holder_t *h, const char *who)
1278
+ {
1279
+ if ( ! ca_mv_holder_each(h, ca_mv_byte_is_bit, NULL) ) {
1280
+ rb_raise(rb_eArgError,
1281
+ "%sboolean MemoryView holds bytes other than 0 and 1; "
1282
+ "a wrap cannot normalise them (CArray.from_memory_view can)", who);
1044
1283
  }
1045
- *out_data_type = data_type;
1046
1284
  }
1047
1285
 
1048
1286
  /* Extract the optional data_type: and mask: keywords from argv.
@@ -1075,421 +1313,284 @@ ca_mv_extract_type_and_mask_kwargs (int argc, VALUE *argv,
1075
1313
  return data_type;
1076
1314
  }
1077
1315
 
1078
- /* Acquire and validate a mask MemoryView against the data side's shape.
1079
- Returns an allocated holder (caller owns; release via the imported
1080
- holder TypedData dfree path) with view active on success.
1081
-
1082
- On validation failure: releases the mask view, frees the mask holder,
1083
- AND ALSO releases the supplied `data_view_to_release_on_fail` + frees
1084
- the supplied `data_holder_to_free_on_fail` before raising. This
1085
- avoids leaking the caller's already-acquired data side when mask
1086
- validation aborts. Pass NULL for the data cleanup args if not
1087
- applicable (e.g. mask validated for a stack-allocated data view in
1088
- the copy path). */
1089
- static ca_mv_imported_holder_t *
1090
- ca_mv_acquire_mask_view (VALUE mask_src, int8_t data_ndim,
1091
- const ca_size_t *data_dim,
1092
- ca_mv_imported_holder_t *data_holder_to_free_on_fail,
1093
- rb_memory_view_t *data_view_to_release_on_fail)
1316
+ typedef struct {
1317
+ VALUE obj, mask_obj, target_class;
1318
+ int8_t explicit_data_type, typeless_data_type;
1319
+ int writable;
1320
+ ca_mv_imported_holder_t *data, *mask;
1321
+ VALUE data_holder_obj, mask_holder_obj;
1322
+ } ca_mv_import_args_t;
1323
+
1324
+ /* The body of wrap_memory_view. Runs under rb_protect; the caller
1325
+ releases both views if it raises. */
1326
+ static VALUE
1327
+ ca_mv_wrap_body (VALUE arg)
1094
1328
  {
1095
- ca_mv_imported_holder_t *holder;
1096
- rb_memory_view_t *mv;
1097
- int i;
1329
+ ca_mv_import_args_t *a = (ca_mv_import_args_t *) arg;
1330
+ ca_mv_imported_holder_t *d = a->data, *m = a->mask;
1331
+ rb_memory_view_t *v = &d->view;
1332
+ ca_size_t bytes;
1333
+ int8_t data_type;
1334
+ volatile VALUE wrap;
1335
+ CArray *ca;
1098
1336
 
1099
- # define MV_MASK_CLEANUP_AND_RAISE(msg, ...) do { \
1100
- if (holder) { \
1101
- if (holder->valid) { rb_memory_view_release(&holder->view); } \
1102
- xfree(holder); \
1103
- } \
1104
- if (data_view_to_release_on_fail) { \
1105
- rb_memory_view_release(data_view_to_release_on_fail); \
1106
- } \
1107
- if (data_holder_to_free_on_fail) { \
1108
- data_holder_to_free_on_fail->valid = false; \
1109
- xfree(data_holder_to_free_on_fail); \
1110
- } \
1111
- rb_raise(rb_eArgError, msg, ##__VA_ARGS__); \
1112
- } while (0)
1113
-
1114
- holder = ALLOC(ca_mv_imported_holder_t);
1115
- holder->valid = false;
1116
-
1117
- /* Zero-init before get: same apache/arrow#45187 defense as the data
1118
- path (release frees view->item_desc.components unconditionally). */
1119
- memset(&holder->view, 0, sizeof(holder->view));
1120
-
1121
- if (! rb_memory_view_get(mask_src, &holder->view, RUBY_MEMORY_VIEW_STRIDES)) {
1122
- if (! rb_memory_view_get(mask_src, &holder->view, RUBY_MEMORY_VIEW_SIMPLE)) {
1123
- MV_MASK_CLEANUP_AND_RAISE("mask: object does not support MemoryView");
1124
- }
1337
+ data_type = ca_mv_holder_take_data(d, a->obj, a->explicit_data_type,
1338
+ a->typeless_data_type, a->writable);
1339
+ bytes = ca_mv_carray_bytes_for(data_type, v->item_size);
1340
+
1341
+ /* A strided producer comes back as a CAStride, so a caller who asked
1342
+ for a class would get something that is not it. */
1343
+ if ( a->target_class != rb_cCAWrap && ! d->contiguous ) {
1344
+ rb_raise(rb_eArgError,
1345
+ "%s was requested, but this producer is strided, so the result "
1346
+ "would be a CAStride; pass a row-major contiguous producer, or "
1347
+ "use CArray.from_memory_view for a copy",
1348
+ rb_class2name(a->target_class));
1125
1349
  }
1126
- holder->valid = true;
1127
- mv = &holder->view;
1128
1350
 
1129
- if (mv->format != NULL) {
1130
- char c = mv->format[0];
1131
- /* skip a leading byte-order/native prefix '<>=!@' */
1132
- if (c == '<' || c == '>' || c == '=' || c == '!' || c == '@') {
1133
- c = mv->format[1];
1351
+ if ( m != NULL ) {
1352
+ ca_mv_holder_take_mask(m, a->mask_obj, d);
1353
+ if ( ! d->contiguous ) {
1354
+ rb_raise(rb_eArgError,
1355
+ "mask: combining mask: with a strided data source is not "
1356
+ "supported in this release; pass a row-major contiguous "
1357
+ "data source");
1134
1358
  }
1135
- /* One byte per element, under either vocabulary: PEP 3118 spells the
1136
- integer widths 'b' / 'B', Ruby's MemoryView spells them 'c' / 'C'.
1137
- '?' is PEP's boolean, which Ruby has no spelling for. */
1138
- if (c != '?' && c != 'B' && c != 'b' && c != 'C' && c != 'c') {
1139
- char fmt_buf[64];
1140
- snprintf(fmt_buf, sizeof(fmt_buf), "%s", mv->format);
1141
- MV_MASK_CLEANUP_AND_RAISE("mask: format must be bool/uint8/int8 (got %s)",
1142
- fmt_buf);
1359
+ /* A mask that shares bytes with the values would take every write
1360
+ to a value as a write to the mask. */
1361
+ if ( d->elements > 0 ) {
1362
+ const char *dlo = (const char *) v->data + d->lo;
1363
+ const char *dhi = (const char *) v->data + d->hi;
1364
+ const char *mlo = (const char *) m->view.data;
1365
+ const char *mhi = mlo + m->elements;
1366
+ if ( mlo < dhi && dlo < mhi ) {
1367
+ rb_raise(rb_eArgError, "mask: the mask buffer overlaps the data buffer");
1368
+ }
1143
1369
  }
1370
+ ca_mv_check_bits(m, "mask: ");
1144
1371
  }
1145
- if (mv->item_size != 1) {
1146
- ssize_t got = mv->item_size;
1147
- MV_MASK_CLEANUP_AND_RAISE("mask: item_size must be 1 (got %ld)",
1148
- (long) got);
1149
- }
1150
- if (mv->ndim != data_ndim) {
1151
- int got = (int) mv->ndim;
1152
- MV_MASK_CLEANUP_AND_RAISE("mask: ndim (%d) does not match data ndim (%d)",
1153
- got, (int) data_ndim);
1154
- }
1155
- if (mv->shape == NULL) {
1156
- /* SIMPLE-form mask (contiguous 1-D, shape omitted). item_size is 1
1157
- (checked above), so byte_size is the element count. */
1158
- ca_size_t mlen = (ca_size_t) mv->byte_size;
1159
- if (data_ndim != 1 || mlen != data_dim[0]) {
1160
- long ms = (long) mlen, ds = (long) (data_ndim == 1 ? data_dim[0] : -1);
1161
- MV_MASK_CLEANUP_AND_RAISE(
1162
- "mask: length (%ld) does not match data shape (%ld)", ms, ds);
1163
- }
1372
+ if ( data_type == CA_BOOLEAN ) {
1373
+ ca_mv_check_bits(d, "");
1164
1374
  }
1165
- else {
1166
- for (i = 0; i < data_ndim; i++) {
1167
- if ((ca_size_t) mv->shape[i] != data_dim[i]) {
1168
- long ms = (long) mv->shape[i], ds = (long) data_dim[i];
1169
- MV_MASK_CLEANUP_AND_RAISE(
1170
- "mask: shape[%d] (%ld) does not match data shape (%ld)",
1171
- i, ms, ds);
1172
- }
1375
+
1376
+ if ( ! d->contiguous ) {
1377
+ /* The foreign bytes the elements reach, [data + lo, data + hi), as a
1378
+ byte array, with the user's CAStride over it. The parent covers
1379
+ every byte a stride can land on, so a mask made for the view (one
1380
+ mask byte per parent byte) has a place for every element whatever
1381
+ the strides are. */
1382
+ ca_size_t span = (ca_size_t) (d->hi - d->lo);
1383
+ ca_size_t strides_arr[CA_RANK_MAX];
1384
+ volatile VALUE inner_wrap;
1385
+ CArray *iw;
1386
+ int8_t k;
1387
+
1388
+ inner_wrap = rb_ca_wrap_new(CA_UINT8, 1, &span, 1, NULL,
1389
+ (char *) v->data + d->lo);
1390
+ rb_ivar_set(inner_wrap, rb_intern("memory_view_source"), a->obj);
1391
+ rb_ivar_set(inner_wrap, rb_intern("memory_view_holder"), a->data_holder_obj);
1392
+ if ( v->readonly && ca_mv_extract(inner_wrap, &iw) ) {
1393
+ ca_set_flag(iw, CA_FLAG_READ_ONLY);
1173
1394
  }
1174
- }
1175
- if (! ca_mv_view_is_contiguous(mv)) {
1176
- MV_MASK_CLEANUP_AND_RAISE(
1177
- "mask: strided masks are not supported in this release; "
1178
- "pass a row-major contiguous mask buffer");
1395
+ for (k = 0; k < d->ndim; k++) {
1396
+ strides_arr[k] = (ca_size_t) d->strides[k];
1397
+ }
1398
+ wrap = rb_ca_stride_new(inner_wrap, data_type, bytes, d->ndim, d->dim,
1399
+ strides_arr, (ca_size_t) -d->lo);
1400
+ if ( v->readonly && ca_mv_extract(wrap, &ca) ) {
1401
+ ca_set_flag(ca, CA_FLAG_READ_ONLY);
1402
+ }
1403
+ return wrap;
1179
1404
  }
1180
1405
 
1181
- # undef MV_MASK_CLEANUP_AND_RAISE
1182
- return holder;
1406
+ /* Same struct rb_ca_wrap_new would build, wrapped in the class the
1407
+ receiver asked for. */
1408
+ wrap = ca_wrap_struct_as(ca_wrap_new(data_type, d->ndim, d->dim, bytes,
1409
+ NULL, (char *) v->data),
1410
+ a->target_class);
1411
+ rb_ivar_set(wrap, rb_intern("memory_view_source"), a->obj);
1412
+ rb_ivar_set(wrap, rb_intern("memory_view_holder"), a->data_holder_obj);
1413
+ ca_mv_extract(wrap, &ca);
1414
+ if ( v->readonly ) {
1415
+ ca_set_flag(ca, CA_FLAG_READ_ONLY);
1416
+ }
1417
+
1418
+ /* Borrowed mask: a CAWrap over the mask bytes, anchored on the same
1419
+ wrap so its lifetime is the data side's. A store writes the mask
1420
+ cell of every element it stores, so a read-only mask makes the whole
1421
+ array read-only. */
1422
+ if ( m != NULL ) {
1423
+ CArray *mask_ca = (CArray *) ca_wrap_new(CA_BOOLEAN, d->ndim, d->dim, 1,
1424
+ NULL, (char *) m->view.data);
1425
+ ca_set_flag(mask_ca, CA_FLAG_MASK_ARRAY);
1426
+ if ( m->view.readonly ) {
1427
+ ca_set_flag(mask_ca, CA_FLAG_READ_ONLY);
1428
+ ca_set_flag(ca, CA_FLAG_READ_ONLY);
1429
+ }
1430
+ ca->mask = mask_ca;
1431
+ rb_ivar_set(wrap, rb_intern("mask_memory_view_source"), a->mask_obj);
1432
+ rb_ivar_set(wrap, rb_intern("mask_memory_view_holder"), a->mask_holder_obj);
1433
+ }
1434
+ return wrap;
1183
1435
  }
1184
1436
 
1185
- /* Copy a strided MemoryView into a contiguous row-major destination buffer.
1186
- Walks indices in row-major order and reads from view->data using
1187
- view->strides at each step. Assumes view->ndim >= 1, view->strides
1188
- non-NULL, dst sized to hold elements * item_size. */
1189
1437
  static void
1190
- ca_mv_copy_strided_to_contiguous (char *dst, const rb_memory_view_t *view,
1191
- ca_size_t elements)
1438
+ ca_mv_import_args_init (ca_mv_import_args_t *a, VALUE obj, VALUE mask_obj)
1192
1439
  {
1193
- int8_t ndim = (int8_t) view->ndim;
1194
- ssize_t item_size = view->item_size;
1195
- ca_size_t idx[CA_RANK_MAX];
1196
- ca_size_t n;
1197
- int8_t k;
1198
-
1199
- for (k = 0; k < ndim; k++) idx[k] = 0;
1200
-
1201
- for (n = 0; n < elements; n++) {
1202
- ssize_t src_off = 0;
1203
- for (k = 0; k < ndim; k++) {
1204
- src_off += (ssize_t) idx[k] * view->strides[k];
1205
- }
1206
- memcpy(dst, (const char *) view->data + src_off, (size_t) item_size);
1207
- dst += item_size;
1208
- /* increment row-major: last dim varies fastest */
1209
- for (k = ndim - 1; k >= 0; k--) {
1210
- idx[k]++;
1211
- if (idx[k] < (ca_size_t) view->shape[k]) break;
1212
- idx[k] = 0;
1213
- }
1440
+ a->obj = obj;
1441
+ a->mask_obj = mask_obj;
1442
+ a->data_holder_obj = ca_mv_holder_new(&a->data);
1443
+ a->mask = NULL;
1444
+ a->mask_holder_obj = Qnil;
1445
+ if ( ! NIL_P(mask_obj) ) {
1446
+ a->mask_holder_obj = ca_mv_holder_new(&a->mask);
1214
1447
  }
1215
1448
  }
1216
1449
 
1217
- /* CArray.wrap_memory_view(obj, data_type: nil) -- zero-copy CAWrap.
1218
- Rejects non-row-major-contiguous sources with a helpful pointer to
1219
- from_memory_view; the contiguous-only contract keeps the boundary
1220
- between Numo/CArray internal invariants clean (see the design note
1221
- on strided wrap rejection).
1450
+ /* Wrap obj's MemoryView without copying. `explicit_data_type` and
1451
+ `typeless_data_type` are as for ca_mv_holder_take_data; `writable`
1452
+ asks the producer for a writable view (wrap_writable). Every view
1453
+ taken is released before a raise leaves. */
1454
+ VALUE
1455
+ ca_mv_wrap (VALUE obj, VALUE mask_obj, VALUE target_class,
1456
+ int8_t explicit_data_type, int8_t typeless_data_type, int writable)
1457
+ {
1458
+ ca_mv_import_args_t a;
1459
+ volatile VALUE result;
1460
+ int tag = 0;
1461
+
1462
+ ca_mv_import_args_init(&a, obj, mask_obj);
1463
+ a.target_class = target_class;
1464
+ a.explicit_data_type = explicit_data_type;
1465
+ a.typeless_data_type = typeless_data_type;
1466
+ a.writable = writable;
1467
+ /* protect: the views are released at once rather than when the holders are collected */
1468
+ result = rb_protect(ca_mv_wrap_body, (VALUE) &a, &tag);
1469
+ if ( tag ) {
1470
+ ca_mv_holder_release(a.mask);
1471
+ ca_mv_holder_release(a.data);
1472
+ rb_jump_tag(tag);
1473
+ }
1474
+ RB_GC_GUARD(a.data_holder_obj);
1475
+ RB_GC_GUARD(a.mask_holder_obj);
1476
+ return result;
1477
+ }
1478
+
1479
+ /* CArray.wrap_memory_view(obj, data_type: nil, mask: nil) -- zero-copy.
1480
+ A row-major contiguous producer comes back as a CAWrap; a strided one
1481
+ as a CAStride over the foreign bytes, its strides (negative ones
1482
+ included) kept.
1222
1483
 
1223
1484
  When the producer is typeless (format=NULL), `data_type:` must be
1224
1485
  supplied; the byte buffer is reinterpreted as that data_type, ndim=1.
1225
1486
 
1226
1487
  The receiver picks the class of the result. Called on CArray itself
1227
- it builds a CAWrap, as it always has; called on a subclass of CAWrap
1228
- it builds that subclass, so a gem bridging a foreign buffer can name
1229
- where the array came from without writing a C extension of its own.
1230
- The class marks the provenance of this object only -- a view derived
1231
- from it is a CABlock or a CAStride like any other. */
1488
+ it builds a CAWrap; called on a subclass of CAWrap it builds that
1489
+ subclass, so a gem bridging a foreign buffer can name where the array
1490
+ came from without writing a C extension of its own. The class marks
1491
+ the provenance of this object only -- a view derived from it is a
1492
+ CABlock or a CAStride like any other. */
1232
1493
  static VALUE
1233
1494
  rb_ca_s_wrap_memory_view (int argc, VALUE *argv, VALUE klass)
1234
1495
  {
1235
- ca_mv_imported_holder_t *holder;
1236
- ca_mv_imported_holder_t *mask_holder = NULL;
1237
- rb_memory_view_t *v;
1238
- int8_t data_type, ndim;
1239
- ca_size_t dim[CA_RANK_MAX];
1240
- VALUE wrap, holder_obj, obj, mask_obj, target_class;
1496
+ VALUE obj, mask_obj, target_class;
1241
1497
  int8_t explicit_data_type =
1242
1498
  ca_mv_extract_type_and_mask_kwargs(argc, argv,
1243
1499
  (VALUE *) &obj, (VALUE *) &mask_obj);
1244
1500
 
1245
- /* Resolve the class before anything is acquired, so this rejection
1246
- needs no cleanup. CArray means CAWrap; any other receiver has to
1247
- be a CAWrap the caller can actually be handed. */
1248
1501
  target_class = ( klass == rb_cCArray ) ? rb_cCAWrap : klass;
1249
1502
  if ( ! RTEST(rb_class_inherited_p(target_class, rb_cCAWrap)) ) {
1250
1503
  rb_raise(rb_eTypeError,
1251
1504
  "wrap_memory_view builds a CAWrap; %s is not a subclass of it",
1252
1505
  rb_class2name(target_class));
1253
1506
  }
1507
+ return ca_mv_wrap(obj, mask_obj, target_class, explicit_data_type, -1, 0);
1508
+ }
1254
1509
 
1255
- holder = ALLOC(ca_mv_imported_holder_t);
1256
- holder->valid = false;
1257
-
1258
- ca_mv_acquire_and_validate(obj, &holder->view, explicit_data_type,
1259
- &data_type, &ndim, dim);
1260
- holder->valid = true;
1261
- v = &holder->view;
1262
-
1263
- /* Whether the producer is strided is only knowable once the view is
1264
- acquired, and a strided one is returned as a CAStride wrapping an
1265
- inner CAWrap -- so a caller who asked for a class would get back
1266
- something that is not it. Refuse instead, and release first: this
1267
- runs before the mask is acquired, so the holder is all there is to
1268
- free. */
1269
- if ( target_class != rb_cCAWrap &&
1270
- v->ndim >= 1 && v->format != NULL && ! ca_mv_view_is_contiguous(v) ) {
1271
- rb_memory_view_release(v);
1272
- holder->valid = false;
1273
- xfree(holder);
1274
- rb_raise(rb_eArgError,
1275
- "%s was requested, but this producer is strided, so the result "
1276
- "would be a CAStride; pass a row-major contiguous producer, or "
1277
- "use CArray.from_memory_view for a copy",
1278
- rb_class2name(target_class));
1279
- }
1510
+ /* The body of from_memory_view. Runs under rb_ensure, which releases
1511
+ both views however it leaves. */
1512
+ static VALUE
1513
+ ca_mv_from_body (VALUE arg)
1514
+ {
1515
+ ca_mv_import_args_t *a = (ca_mv_import_args_t *) arg;
1516
+ ca_mv_imported_holder_t *d = a->data, *m = a->mask;
1517
+ rb_memory_view_t *v = &d->view;
1518
+ int8_t data_type;
1519
+ volatile VALUE result;
1520
+ CArray *ca;
1521
+ ca_size_t i;
1280
1522
 
1281
- /* mask: paired buffer. Acquire + validate now (before building the
1282
- wrap). On failure the helper releases both the mask view AND the
1283
- already-acquired data view + holder, then raises. */
1284
- if (! NIL_P(mask_obj)) {
1285
- mask_holder = ca_mv_acquire_mask_view(mask_obj, ndim, dim,
1286
- holder, &holder->view);
1287
- /* Strided data side combined with mask: is not supported in this
1288
- release. Catch this AFTER mask acquisition so the cleanup path can release
1289
- both views uniformly. */
1290
- if (v->ndim >= 1 && v->format != NULL &&
1291
- ! ca_mv_view_is_contiguous(v)) {
1292
- rb_memory_view_release(&mask_holder->view);
1293
- mask_holder->valid = false;
1294
- xfree(mask_holder);
1295
- rb_memory_view_release(v);
1296
- holder->valid = false;
1297
- xfree(holder);
1298
- rb_raise(rb_eArgError,
1299
- "mask: combining mask: with a strided data source is not "
1300
- "supported in this release; pass a row-major contiguous "
1301
- "data source");
1302
- }
1523
+ data_type = ca_mv_holder_take_data(d, a->obj, a->explicit_data_type, -1, 0);
1524
+ if ( m != NULL ) {
1525
+ ca_mv_holder_take_mask(m, a->mask_obj, d);
1303
1526
  }
1304
1527
 
1305
- /* Strided producer: build a CAStride on top of an inner CAWrap that
1306
- anchors the foreign memory. The inner CAWrap holds the MemoryView
1307
- lifecycle (memory_view_source + memory_view_holder ivars); the
1308
- outer CAStride is what the user sees, with full strides
1309
- (including negative) preserved. */
1310
- if (v->ndim >= 1 && v->format != NULL &&
1311
- ! ca_mv_view_is_contiguous(v)) {
1312
- ca_size_t inner_dim[1] = { 1 };
1313
- volatile VALUE inner_wrap;
1314
- ca_size_t strides_arr[CA_RANK_MAX];
1315
- int i;
1316
-
1317
- inner_wrap = rb_ca_wrap_new(data_type, 1, inner_dim,
1318
- ca_mv_carray_bytes_for(data_type, v->item_size),
1319
- NULL, (char *) v->data);
1320
- rb_ivar_set(inner_wrap, rb_intern("memory_view_source"), obj);
1321
- holder_obj = TypedData_Wrap_Struct(rb_cObject,
1322
- &ca_mv_imported_holder_data_type,
1323
- holder);
1324
- rb_ivar_set(inner_wrap, rb_intern("memory_view_holder"), holder_obj);
1325
- if (v->readonly) {
1326
- CArray *iw;
1327
- if (ca_mv_extract(inner_wrap, &iw)) {
1328
- ca_set_flag(iw, CA_FLAG_READ_ONLY);
1329
- }
1330
- }
1331
- for (i = 0; i < ndim; i++) {
1332
- strides_arr[i] = (ca_size_t) v->strides[i];
1333
- }
1334
- {
1335
- VALUE result =
1336
- rb_ca_stride_new(inner_wrap, data_type,
1337
- ca_mv_carray_bytes_for(data_type, v->item_size),
1338
- ndim, dim, strides_arr, /* base_offset */ 0);
1339
- if (v->readonly) {
1340
- CArray *rs;
1341
- if (ca_mv_extract(result, &rs)) {
1342
- ca_set_flag(rs, CA_FLAG_READ_ONLY);
1343
- }
1344
- }
1345
- return result;
1346
- }
1347
- }
1528
+ result = rb_carray_new(data_type, d->ndim, d->dim,
1529
+ ca_mv_carray_bytes_for(data_type, v->item_size), NULL);
1530
+ ca_mv_extract(result, &ca);
1348
1531
 
1349
- /* Same struct rb_ca_wrap_new would build, wrapped in the class the
1350
- receiver asked for. Everything downstream -- the holder and source
1351
- ivars, the read-only flag, the borrowed mask -- is untouched, which
1352
- is the point: those are what a gem building its own wrap by hand
1353
- has to give up. */
1354
- wrap = ca_wrap_struct_as(
1355
- ca_wrap_new(data_type, ndim, dim,
1356
- ca_mv_carray_bytes_for(data_type, v->item_size),
1357
- NULL, (char *) v->data),
1358
- target_class);
1359
-
1360
- rb_ivar_set(wrap, rb_intern("memory_view_source"), obj);
1361
- holder_obj = TypedData_Wrap_Struct(rb_cObject,
1362
- &ca_mv_imported_holder_data_type,
1363
- holder);
1364
- rb_ivar_set(wrap, rb_intern("memory_view_holder"), holder_obj);
1365
-
1366
- if (v->readonly) {
1367
- CArray *ca;
1368
- if (ca_mv_extract(wrap, &ca)) {
1369
- ca_set_flag(ca, CA_FLAG_READ_ONLY);
1370
- }
1532
+ if ( d->contiguous ) {
1533
+ memcpy(ca->ptr, v->data, (size_t) ca_length(ca));
1534
+ }
1535
+ else {
1536
+ ca_mv_gather_t g;
1537
+ g.dst = ca->ptr;
1538
+ g.item_size = (size_t) v->item_size;
1539
+ ca_mv_holder_each(d, ca_mv_gather_one, &g);
1540
+ }
1541
+ if ( data_type == CA_BOOLEAN ) {
1542
+ ca_boolean_normalize_bytes(ca->ptr, ca->elements);
1371
1543
  }
1372
1544
 
1373
- /* Attach borrowed mask: build a CAWrap over the mask buffer and assign
1374
- it to wrap->mask. Anchor the mask MV's source + holder on the same
1375
- wrap object so the lifecycle is co-terminal with the data side.
1376
- When `wrap` is collected: free_ca_wrap recurses into ca->mask (frees
1377
- the mask wrap struct, leaves its borrowed ptr alone); the holder
1378
- ivars become unreachable and are GC'd separately, each holder.dfree
1379
- calling rb_memory_view_release. */
1380
- if (mask_holder != NULL) {
1381
- CArray *data_ca, *mask_ca;
1382
- rb_memory_view_t *mv = &mask_holder->view;
1383
- VALUE mask_holder_obj;
1384
- if (! ca_mv_extract(wrap, &data_ca)) {
1385
- rb_memory_view_release(mv);
1386
- mask_holder->valid = false;
1387
- xfree(mask_holder);
1388
- rb_raise(rb_eRuntimeError,
1389
- "internal: rb_ca_wrap_new did not return a CArray");
1390
- }
1391
- mask_ca = (CArray *) ca_wrap_new(CA_BOOLEAN, ndim, dim, 1, NULL,
1392
- (char *) mv->data);
1393
- ca_set_flag(mask_ca, CA_FLAG_MASK_ARRAY);
1394
- if (mv->readonly) {
1395
- ca_set_flag(mask_ca, CA_FLAG_READ_ONLY);
1545
+ /* Mask bytes: any non-zero byte means masked. */
1546
+ if ( m != NULL ) {
1547
+ const uint8_t *src = (const uint8_t *) m->view.data;
1548
+ boolean8_t *dst;
1549
+ ca_create_mask(ca);
1550
+ dst = (boolean8_t *) ca->mask->ptr;
1551
+ for (i = 0; i < ca->elements; i++) {
1552
+ dst[i] = ( src[i] != 0 ) ? 1 : 0;
1396
1553
  }
1397
- data_ca->mask = mask_ca;
1398
-
1399
- rb_ivar_set(wrap, rb_intern("mask_memory_view_source"), mask_obj);
1400
- mask_holder_obj = TypedData_Wrap_Struct(rb_cObject,
1401
- &ca_mv_imported_holder_data_type,
1402
- mask_holder);
1403
- rb_ivar_set(wrap, rb_intern("mask_memory_view_holder"), mask_holder_obj);
1404
1554
  }
1555
+ return result;
1556
+ }
1405
1557
 
1406
- return wrap;
1558
+ static VALUE
1559
+ ca_mv_from_release (VALUE arg)
1560
+ {
1561
+ ca_mv_import_args_t *a = (ca_mv_import_args_t *) arg;
1562
+ ca_mv_holder_release(a->mask);
1563
+ ca_mv_holder_release(a->data);
1564
+ return Qnil;
1407
1565
  }
1408
1566
 
1409
- /* CArray.from_memory_view(obj, data_type: nil) -- copy into an independent CArray.
1410
- Accepts strided typed sources (gather copy) and typeless sources
1411
- (must be 1D contiguous; bytes reinterpreted as `data_type:`). */
1567
+ /* CArray.from_memory_view(obj, data_type: nil, mask: nil) -- copy into an
1568
+ independent CArray. Accepts strided typed sources (gather copy) and
1569
+ typeless sources (bytes reinterpreted as `data_type:`). The result is
1570
+ a plain CArray whatever the receiver: a copy no longer borrows anything
1571
+ for a class to name. A receiver that is not CArray or a CAWrap could
1572
+ not be returned, so it is refused. */
1412
1573
  static VALUE
1413
1574
  rb_ca_s_from_memory_view (int argc, VALUE *argv, VALUE klass)
1414
1575
  {
1415
- rb_memory_view_t view;
1416
- ca_mv_imported_holder_t *mask_holder = NULL;
1417
- int8_t data_type, ndim;
1418
- ca_size_t dim[CA_RANK_MAX];
1419
- VALUE result, obj, mask_obj;
1420
- CArray *ca;
1421
- bool contig;
1576
+ ca_mv_import_args_t a;
1577
+ VALUE obj, mask_obj;
1578
+ volatile VALUE result;
1422
1579
  int8_t explicit_data_type =
1423
1580
  ca_mv_extract_type_and_mask_kwargs(argc, argv,
1424
1581
  (VALUE *) &obj, (VALUE *) &mask_obj);
1425
1582
 
1426
- ca_mv_acquire_and_validate(obj, &view, explicit_data_type,
1427
- &data_type, &ndim, dim);
1428
-
1429
- /* mask: acquire + validate now so failures clean up the data view too. */
1430
- if (! NIL_P(mask_obj)) {
1431
- mask_holder = ca_mv_acquire_mask_view(mask_obj, ndim, dim,
1432
- NULL, &view);
1433
- }
1434
-
1435
- result = rb_carray_new(data_type, ndim, dim,
1436
- ca_mv_carray_bytes_for(data_type, view.item_size),
1437
- NULL);
1438
- if (! ca_mv_extract(result, &ca)) {
1439
- rb_memory_view_release(&view);
1440
- rb_raise(rb_eRuntimeError, "internal: rb_carray_new did not return a CArray");
1441
- }
1442
-
1443
- /* Typeless producers (format == NULL) and SIMPLE-form producers (shape
1444
- omitted) are 1-D contiguous by construction; ca_mv_view_is_contiguous
1445
- handles the NULL shape that would crash the raw contiguity helper. */
1446
- contig = (view.format == NULL) || ca_mv_view_is_contiguous(&view);
1447
-
1448
- if (contig) {
1449
- size_t total_bytes = (size_t) ca_length(ca);
1450
- /* For typeless producers byte_size and total_bytes are equal by
1451
- construction; for typed producers we already validated alignment. */
1452
- if (view.byte_size < (ssize_t) total_bytes) {
1453
- rb_memory_view_release(&view);
1454
- if (mask_holder) {
1455
- rb_memory_view_release(&mask_holder->view);
1456
- mask_holder->valid = false;
1457
- xfree(mask_holder);
1458
- }
1459
- rb_raise(rb_eRuntimeError,
1460
- "internal: byte_size mismatch (carray=%lu, view=%ld)",
1461
- (unsigned long) total_bytes, (long) view.byte_size);
1462
- }
1463
- memcpy(ca->ptr, view.data, total_bytes);
1464
- }
1465
- else {
1466
- /* Strided typed source: walk indices in row-major order and gather. */
1467
- ca_mv_copy_strided_to_contiguous(ca->ptr, &view, ca->elements);
1468
- }
1469
-
1470
- rb_memory_view_release(&view);
1471
-
1472
- /* mask: materialise the canonical CA_BOOLEAN mask slot via ca_create_mask,
1473
- then memcpy bytes from the (validated row-major contig 1-byte/elem) mask
1474
- buffer. Source format ? / B / b all use byte values where any non-zero
1475
- means masked; that semantic matches CArray's mask convention directly
1476
- when we coerce non-zero to 1. */
1477
- if (mask_holder) {
1478
- rb_memory_view_t *mv = &mask_holder->view;
1479
- size_t n = (size_t) ca->elements;
1480
- size_t i;
1481
- const uint8_t *src = (const uint8_t *) mv->data;
1482
- boolean8_t *dst;
1483
- ca_create_mask(ca);
1484
- dst = (boolean8_t *) ca->mask->ptr;
1485
- for (i = 0; i < n; i++) {
1486
- dst[i] = (src[i] != 0) ? 1 : 0;
1487
- }
1488
- rb_memory_view_release(mv);
1489
- mask_holder->valid = false;
1490
- xfree(mask_holder);
1491
- }
1492
-
1583
+ if ( klass != rb_cCArray &&
1584
+ ! RTEST(rb_class_inherited_p(klass, rb_cCAWrap)) ) {
1585
+ rb_raise(rb_eTypeError,
1586
+ "from_memory_view builds a CArray; %s cannot be returned from it",
1587
+ rb_class2name(klass));
1588
+ }
1589
+ ca_mv_import_args_init(&a, obj, mask_obj);
1590
+ a.explicit_data_type = explicit_data_type;
1591
+ result = rb_ensure(ca_mv_from_body, (VALUE) &a, ca_mv_from_release, (VALUE) &a);
1592
+ RB_GC_GUARD(a.data_holder_obj);
1593
+ RB_GC_GUARD(a.mask_holder_obj);
1493
1594
  return result;
1494
1595
  }
1495
1596
 
@@ -1512,7 +1613,7 @@ rb_ca_s_memory_view_reject_reason (VALUE klass, VALUE obj)
1512
1613
  CArray *ca;
1513
1614
  ca_mv_reject_t rej;
1514
1615
  if (! ca_mv_extract(obj, &ca)) {
1515
- return rb_str_new_cstr("not a CArray");
1616
+ return Qnil;
1516
1617
  }
1517
1618
  if (! ca_mv_ca_exportable(ca, obj)) {
1518
1619
  if (ca->data_type == CA_FIXLEN && RTEST(rb_ca_has_data_class(obj))) {
@@ -1530,17 +1631,18 @@ rb_ca_s_memory_view_reject_reason (VALUE klass, VALUE obj)
1530
1631
  }
1531
1632
  if (! ca_mv_check_alias_chain(ca, &rej)) {
1532
1633
  const char *where;
1533
- if (rej.depth == 0) where = "this view";
1634
+ if (rej.depth < 0) where = "the root";
1635
+ else if (rej.depth == 0) where = "this view";
1534
1636
  else if (rej.depth == 1) where = "parent";
1535
1637
  else where = NULL; /* ancestor[N] */
1536
1638
  if (where) {
1537
- return rb_sprintf("%s is %s (%s); zero-copy wrap not possible. "
1538
- "Use CArray.from_memory_view(arr) or arr.to_ca for a snapshot.",
1539
- where, rej.class_name, rej.reason);
1639
+ return rb_sprintf("%s is %s (%s); zero-copy wrap not possible.%s",
1640
+ where, rej.class_name, rej.reason,
1641
+ rej.face ? "" : " Export arr.copy for a snapshot.");
1540
1642
  } else {
1541
- return rb_sprintf("ancestor[%d] is %s (%s); zero-copy wrap not possible. "
1542
- "Use CArray.from_memory_view(arr) or arr.to_ca for a snapshot.",
1543
- rej.depth - 1, rej.class_name, rej.reason);
1643
+ return rb_sprintf("ancestor[%d] is %s (%s); zero-copy wrap not possible.%s",
1644
+ rej.depth - 1, rej.class_name, rej.reason,
1645
+ rej.face ? "" : " Export arr.copy for a snapshot.");
1544
1646
  }
1545
1647
  }
1546
1648
  return Qnil;
@@ -1566,8 +1668,8 @@ rb_ca_s_parse_memory_view_format (VALUE klass, VALUE fmt_v, VALUE item_size_v)
1566
1668
 
1567
1669
  /* Test hook: return the MV format string the producer would emit
1568
1670
  for `obj`, without actually building the rb_memory_view_t. Used
1569
- by spec_ai to verify PEP 3118 struct format emission for CAStruct
1570
- arrays. Returns nil if obj isn't exportable. */
1671
+ by spec_ai to verify the record format emitted for CAStruct arrays.
1672
+ Returns nil if obj isn't exportable. */
1571
1673
  static VALUE
1572
1674
  rb_ca_s_memory_view_format (VALUE klass, VALUE obj)
1573
1675
  {
@@ -1578,24 +1680,30 @@ rb_ca_s_memory_view_format (VALUE klass, VALUE obj)
1578
1680
  return ca_mv_struct_format_for_data_class(rb_ca_data_class(obj));
1579
1681
  }
1580
1682
  if (ca->data_type == CA_FIXLEN) {
1581
- /* Plain CA_FIXLEN: emit "Ns" via the same caching scheme used by
1582
- the real producer (ca_mv_get). Reusing the cache keeps the
1583
- returned String identity-equal to the one the producer hands out
1584
- via view->format, which test code may rely on. */
1585
- VALUE fmt_str = rb_attr_get(obj, rb_intern("__mv_fixlen_format__"));
1586
- if (NIL_P(fmt_str)) {
1587
- char fmt_buf[32];
1588
- snprintf(fmt_buf, sizeof(fmt_buf), "%lds", (long) ca->bytes);
1589
- fmt_str = rb_str_new_cstr(fmt_buf);
1590
- rb_obj_freeze(fmt_str);
1591
- rb_ivar_set(obj, rb_intern("__mv_fixlen_format__"), fmt_str);
1592
- }
1593
- return fmt_str;
1683
+ /* Plain CA_FIXLEN: the same "Ns" String the producer (ca_mv_get)
1684
+ hands out via view->format. */
1685
+ return rb_obj_freeze(rb_str_new_cstr(ca_mv_fixlen_format(ca->bytes)));
1594
1686
  }
1595
1687
  const char *fmt = ca_mv_format_for(ca->data_type);
1596
1688
  return fmt ? rb_str_new_cstr(fmt) : Qnil;
1597
1689
  }
1598
1690
 
1691
+ /* CARecord#memory_view_field_names: the names of the record's members,
1692
+ one per specifier of the record's MemoryView format, in that order.
1693
+ Ruby's format has no place for them, so a bridge that writes a format
1694
+ which does (PEP 3118 "T{...}") asks the exporting object for them under
1695
+ this name. nil when the record has no MemoryView format. */
1696
+ static VALUE
1697
+ rb_ca_record_memory_view_field_names (VALUE self)
1698
+ {
1699
+ VALUE data_class = rb_ca_data_class(self);
1700
+ if ( NIL_P(data_class)
1701
+ || NIL_P(ca_mv_struct_format_for_data_class(data_class)) ) {
1702
+ return Qnil;
1703
+ }
1704
+ return rb_ary_dup(rb_const_get(data_class, rb_intern("MEMBERS")));
1705
+ }
1706
+
1599
1707
  /* ---------------- Init ---------------- */
1600
1708
 
1601
1709
  static const rb_memory_view_entry_t ca_memory_view_entry = {
@@ -1607,6 +1715,8 @@ static const rb_memory_view_entry_t ca_memory_view_entry = {
1607
1715
  void
1608
1716
  Init_carray_memory_view (void)
1609
1717
  {
1718
+ ca_mv_fixlen_formats = st_init_numtable();
1719
+ rb_nativethread_lock_initialize(&ca_mv_fixlen_formats_lock);
1610
1720
  /* Statically known classes. */
1611
1721
  rb_memory_view_register(rb_cCArray, &ca_memory_view_entry);
1612
1722
  rb_memory_view_register(rb_cCAWrap, &ca_memory_view_entry);
@@ -1640,11 +1750,15 @@ Init_carray_memory_view (void)
1640
1750
  rb_ca_s_from_memory_view, -1);
1641
1751
  rb_define_singleton_method(rb_cCArray, "wrap_memory_view",
1642
1752
  rb_ca_s_wrap_memory_view, -1);
1643
- /* Test hooks (used by spec_ai/test_format_parser.rb and
1644
- spec_ai/test_mv_struct_format.rb). Private API; names start
1645
- with __ to discourage external use. */
1646
- rb_define_singleton_method(rb_cCArray, "__memory_view_parse_format__",
1647
- rb_ca_s_parse_memory_view_format, 2);
1648
- rb_define_singleton_method(rb_cCArray, "__memory_view_format__",
1649
- rb_ca_s_memory_view_format, 1);
1753
+ /* The format parser and the format an array exports, for the specs
1754
+ (test_format_parser.rb, test_mv_struct_format.rb). Private. */
1755
+ rb_define_private_method(rb_singleton_class(rb_cCArray),
1756
+ "parse_memory_view_format",
1757
+ rb_ca_s_parse_memory_view_format, 2);
1758
+ rb_define_private_method(rb_singleton_class(rb_cCArray),
1759
+ "memory_view_format_of",
1760
+ rb_ca_s_memory_view_format, 1);
1761
+ rb_define_method(rb_const_get(rb_cObject, rb_intern("CARecord")),
1762
+ "memory_view_field_names",
1763
+ rb_ca_record_memory_view_field_names, 0);
1650
1764
  }