carray 3.0.1 → 3.0.3

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 (229) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +824 -0
  3. data/README.md +5 -6
  4. data/carray.gemspec +1 -1
  5. data/ext/ca_axis_dispatch.c +258 -4
  6. data/ext/ca_axis_group.c +368 -184
  7. data/ext/ca_binop_dispatch.c +54 -32
  8. data/ext/ca_binop_dispatch.h +1 -0
  9. data/ext/ca_categorical_iterator.c +90 -41
  10. data/ext/ca_compare.c +8 -2
  11. data/ext/ca_for_buffer.h +21 -68
  12. data/ext/ca_group_iter.c +27 -11
  13. data/ext/ca_kernel_iterator.c +960 -245
  14. data/ext/ca_kernel_iterator.h +352 -181
  15. data/ext/ca_monop_dispatch.c +4 -4
  16. data/ext/ca_monop_dispatch.h +4 -0
  17. data/ext/ca_obj_array.c +194 -42
  18. data/ext/ca_obj_bincmp.c +152 -121
  19. data/ext/ca_obj_binop.c +107 -210
  20. data/ext/ca_obj_bitarray.c +37 -24
  21. data/ext/ca_obj_bitfield.c +95 -62
  22. data/ext/ca_obj_block.c +5 -4
  23. data/ext/ca_obj_byte_swap.c +40 -40
  24. data/ext/ca_obj_const_string.c +180 -50
  25. data/ext/ca_obj_face.c +428 -62
  26. data/ext/ca_obj_face.h +107 -47
  27. data/ext/ca_obj_fake.c +21 -50
  28. data/ext/ca_obj_farray.c +1 -0
  29. data/ext/ca_obj_field.c +16 -3
  30. data/ext/ca_obj_fixlen_string.c +39 -26
  31. data/ext/ca_obj_grid.c +181 -124
  32. data/ext/ca_obj_meld.c +235 -55
  33. data/ext/ca_obj_moncmp.c +61 -61
  34. data/ext/ca_obj_monop.c +125 -107
  35. data/ext/ca_obj_object.c +551 -233
  36. data/ext/ca_obj_record.c +51 -16
  37. data/ext/ca_obj_reduce.c +5 -8
  38. data/ext/ca_obj_refer.c +117 -33
  39. data/ext/ca_obj_remap.c +57 -17
  40. data/ext/ca_obj_repeat.c +21 -5
  41. data/ext/ca_obj_roll.c +115 -11
  42. data/ext/ca_obj_select.c +88 -75
  43. data/ext/ca_obj_select_axis.c +58 -60
  44. data/ext/ca_obj_shift.c +22 -102
  45. data/ext/ca_obj_stack.c +223 -123
  46. data/ext/ca_obj_stride.c +267 -35
  47. data/ext/ca_obj_string.c +25 -22
  48. data/ext/ca_obj_tile.c +32 -16
  49. data/ext/ca_obj_time.c +23 -25
  50. data/ext/ca_obj_timedelta.c +21 -23
  51. data/ext/ca_obj_transpose.c +12 -3
  52. data/ext/ca_obj_triop.c +110 -117
  53. data/ext/ca_obj_window.c +157 -29
  54. data/ext/ca_op_ipower.c +25 -13
  55. data/ext/ca_op_powi.h +57 -30
  56. data/ext/ca_pow5_table.h +656 -0
  57. data/ext/ca_rng_normal.h +42 -0
  58. data/ext/ca_rng_xoshiro256pp.h +105 -0
  59. data/ext/ca_sort_kernels.h +70 -0
  60. data/ext/ca_sweep_engine.c +385 -198
  61. data/ext/ca_sweep_engine.h +31 -23
  62. data/ext/ca_triop_dispatch.c +4 -1
  63. data/ext/ca_triop_dispatch.h +5 -2
  64. data/ext/caframe_csv_reader.c +463 -0
  65. data/ext/carray.h +90 -28
  66. data/ext/carray_access.c +487 -231
  67. data/ext/carray_address_basis.c +731 -0
  68. data/ext/carray_attribute.c +45 -3
  69. data/ext/carray_bincount.c +32 -2
  70. data/ext/carray_broadcast.c +49 -17
  71. data/ext/carray_call_cfunc.c +667 -511
  72. data/ext/carray_cast.c +1556 -247
  73. data/ext/carray_cast_func.rb +34 -21
  74. data/ext/carray_conversion.c +197 -73
  75. data/ext/carray_copy.c +84 -38
  76. data/ext/carray_core.c +591 -119
  77. data/ext/carray_count.c +148 -96
  78. data/ext/carray_element.c +18 -27
  79. data/ext/carray_factorize.c +604 -291
  80. data/ext/carray_generate.c +76 -40
  81. data/ext/carray_histogram.c +152 -94
  82. data/ext/carray_hold.c +2 -1
  83. data/ext/carray_index_classifier.c +63 -96
  84. data/ext/carray_internal.h +400 -1
  85. data/ext/carray_lazy.c +834 -79
  86. data/ext/carray_loop.c +88 -66
  87. data/ext/carray_mask.c +705 -324
  88. data/ext/carray_math_kernel.h +1 -0
  89. data/ext/carray_median_percentile.c +263 -63
  90. data/ext/carray_memory_view.c +254 -248
  91. data/ext/carray_operator.c +1093 -363
  92. data/ext/carray_order.c +133 -69
  93. data/ext/carray_partition.c +102 -56
  94. data/ext/carray_random.c +582 -204
  95. data/ext/carray_scatter.c +308 -65
  96. data/ext/carray_segment.c +257 -0
  97. data/ext/carray_slab.c +183 -70
  98. data/ext/carray_sort.c +235 -240
  99. data/ext/carray_sort_kernel.c +8 -1
  100. data/ext/carray_struct.c +99 -21
  101. data/ext/carray_test.c +167 -64
  102. data/ext/carray_time_text.c +1182 -0
  103. data/ext/carray_utils.c +363 -98
  104. data/ext/extconf.rb +27 -2
  105. data/ext/mk_call_cfunc.rb +103 -118
  106. data/ext/mk_pow5_table.rb +35 -0
  107. data/ext/mkkernel.rb +1349 -851
  108. data/ext/ruby_carray.c +17 -2
  109. data/ext/version.h +4 -4
  110. data/ext/version.rb +4 -2
  111. data/lib/carray/attribute.rb +39 -0
  112. data/lib/carray/autoload_carray.rb +24 -3
  113. data/lib/carray/autoload_method_extension.rb +18 -0
  114. data/lib/carray/axis_group.rb +245 -20
  115. data/lib/carray/basics.rb +95 -25
  116. data/lib/carray/bincount_nd.rb +6 -0
  117. data/lib/carray/block_iterator.rb +200 -54
  118. data/lib/carray/boolean_reduce.rb +21 -29
  119. data/lib/carray/categorical.rb +244 -59
  120. data/lib/carray/categorical_iterator.rb +391 -757
  121. data/lib/carray/complex.rb +36 -29
  122. data/lib/carray/conditional.rb +94 -7
  123. data/lib/carray/const_string.rb +177 -27
  124. data/lib/carray/construct.rb +40 -0
  125. data/lib/carray/data_type_extension.rb +3 -0
  126. data/lib/carray/data_type_limits.rb +91 -0
  127. data/lib/carray/fixlen_string.rb +1 -1
  128. data/lib/carray/frame/concat.rb +157 -6
  129. data/lib/carray/frame/convert.rb +13 -7
  130. data/lib/carray/frame/csv_parser.rb +590 -76
  131. data/lib/carray/frame/describe.rb +79 -0
  132. data/lib/carray/frame/frame.rb +205 -35
  133. data/lib/carray/frame/group.rb +199 -40
  134. data/lib/carray/frame/io.rb +452 -60
  135. data/lib/carray/frame/join.rb +134 -46
  136. data/lib/carray/frame/records.rb +54 -19
  137. data/lib/carray/frame/reshape.rb +448 -0
  138. data/lib/carray/frame/sort.rb +26 -2
  139. data/lib/carray/frame/verbs.rb +726 -56
  140. data/lib/carray/frame.rb +2 -0
  141. data/lib/carray/fuse_source.rb +111 -2
  142. data/lib/carray/fusion.rb +229 -38
  143. data/lib/carray/inspect.rb +71 -20
  144. data/lib/carray/iterator.rb +157 -2
  145. data/lib/carray/lazy.rb +54 -67
  146. data/lib/carray/mask_gap_fill.rb +66 -50
  147. data/lib/carray/math.rb +32 -19
  148. data/lib/carray/meld_reduce.rb +40 -7
  149. data/lib/carray/methods/align_addr.rb +8 -4
  150. data/lib/carray/methods/bincount.rb +2 -2
  151. data/lib/carray/methods/bit_string.rb +14 -3
  152. data/lib/carray/methods/composition.rb +16 -10
  153. data/lib/carray/methods/discovery_along.rb +74 -0
  154. data/lib/carray/methods/factorize.rb +50 -0
  155. data/lib/carray/methods/gather_nd.rb +13 -2
  156. data/lib/carray/methods/insert_block.rb +9 -14
  157. data/lib/carray/methods/is_in.rb +130 -6
  158. data/lib/carray/methods/join.rb +1 -1
  159. data/lib/carray/methods/locate_addr.rb +235 -21
  160. data/lib/carray/methods/mask_duplicates.rb +35 -1
  161. data/lib/carray/methods/mode.rb +18 -1
  162. data/lib/carray/methods/nunique.rb +22 -1
  163. data/lib/carray/methods/repeat.rb +91 -0
  164. data/lib/carray/methods/resize.rb +21 -23
  165. data/lib/carray/methods/snap.rb +34 -12
  166. data/lib/carray/methods/unique.rb +52 -3
  167. data/lib/carray/record_order.rb +208 -0
  168. data/lib/carray/rng.rb +86 -0
  169. data/lib/carray/runtime.rb +14 -11
  170. data/lib/carray/segment_iterator.rb +990 -0
  171. data/lib/carray/serialize.rb +197 -27
  172. data/lib/carray/slab_iterator.rb +109 -40
  173. data/lib/carray/stack.rb +10 -4
  174. data/lib/carray/string_operation_extension.rb +9 -4
  175. data/lib/carray/struct.rb +142 -51
  176. data/lib/carray/struct_builder.rb +110 -14
  177. data/lib/carray/table.rb +2 -2
  178. data/lib/carray/time.rb +535 -105
  179. data/lib/carray/window_iterator.rb +259 -82
  180. data/lib/carray.rb +4 -2
  181. data/yard-stubs/ca_obj_array.rb +4 -4
  182. data/yard-stubs/ca_obj_bitarray.rb +7 -0
  183. data/yard-stubs/ca_obj_bitfield.rb +23 -12
  184. data/yard-stubs/ca_obj_block.rb +2 -7
  185. data/yard-stubs/ca_obj_fake.rb +1 -1
  186. data/yard-stubs/ca_obj_field.rb +1 -1
  187. data/yard-stubs/ca_obj_refer.rb +3 -3
  188. data/yard-stubs/ca_obj_roll.rb +0 -1
  189. data/yard-stubs/ca_obj_stride.rb +10 -7
  190. data/yard-stubs/ca_obj_transpose.rb +1 -1
  191. data/yard-stubs/ca_obj_window.rb +11 -4
  192. data/yard-stubs/carray_access.rb +6 -5
  193. data/yard-stubs/carray_broadcast.rb +2 -2
  194. data/yard-stubs/carray_cast.rb +78 -43
  195. data/yard-stubs/carray_conversion.rb +13 -1
  196. data/yard-stubs/carray_copy.rb +8 -3
  197. data/yard-stubs/carray_core.rb +3 -83
  198. data/yard-stubs/carray_count.rb +16 -2
  199. data/yard-stubs/carray_lazy.rb +210 -1
  200. data/yard-stubs/carray_loop.rb +4 -1
  201. data/yard-stubs/carray_mask.rb +34 -10
  202. data/yard-stubs/carray_math.rb +1499 -4
  203. data/yard-stubs/carray_median_percentile.rb +26 -8
  204. data/yard-stubs/carray_order.rb +43 -82
  205. data/yard-stubs/carray_scatter.rb +10 -3
  206. data/yard-stubs/carray_segment.rb +72 -0
  207. data/yard-stubs/carray_slab.rb +11 -8
  208. data/yard-stubs/carray_sort.rb +50 -8
  209. data/yard-stubs/carray_test.rb +10 -0
  210. data/yard-stubs/carray_utils.rb +0 -26
  211. metadata +19 -19
  212. data/ext/ca_for_each_element.h +0 -239
  213. data/ext/carray_build_flags.h +0 -3
  214. data/ext/carray_config.h +0 -28
  215. data/ext/carray_kernels_bincmp.c +0 -4446
  216. data/ext/carray_kernels_binop.c +0 -11001
  217. data/ext/carray_kernels_init.c +0 -1131
  218. data/ext/carray_kernels_map.c +0 -3467
  219. data/ext/carray_kernels_moncmp.c +0 -2097
  220. data/ext/carray_kernels_monop.c +0 -18313
  221. data/ext/carray_kernels_reduce_aggregate.c +0 -25837
  222. data/ext/carray_kernels_reduce_boolean.c +0 -330
  223. data/ext/carray_kernels_reduce_cumulative.c +0 -14593
  224. data/ext/carray_kernels_reduce_extreme.c +0 -16948
  225. data/ext/carray_kernels_reduce_variance.c +0 -3910
  226. data/ext/carray_kernels_scan.c +0 -3693
  227. data/ext/carray_kernels_search.c +0 -32138
  228. data/ext/carray_kernels_sort.c +0 -10626
  229. data/ext/carray_kernels_triop.c +0 -1392
@@ -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,6 +29,7 @@
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"
30
34
  #include <string.h>
31
35
 
@@ -55,12 +59,10 @@ typedef struct {
55
59
 
56
60
  /* ---------------- obj_type strategy ---------------- */
57
61
 
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. */
62
+ /* Strategy = label only. The export is alias-only and is decided by
63
+ ca_mv_compose_to_entity alone; the labels say which classes get a
64
+ MemoryView registration and keep ca_mv_runtime_types[] readable.
65
+ Every class labelled CA_MV_ATTACH is refused by that resolver. */
64
66
  typedef enum {
65
67
  CA_MV_REJECT,
66
68
  CA_MV_DIRECT, /* entity / wrap / scalar; ca->ptr valid as-is */
@@ -72,9 +74,8 @@ typedef enum {
72
74
  CA_MV_ATTACH, /* unreachable: rejected by alias-chain check */
73
75
  } ca_mv_strategy_t;
74
76
 
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. */
77
+ /* Resolved at Init_carray_memory_view. Runtime obj_type ids are stored
78
+ here so ca_mv_reject_reason_for can name the class it refuses. */
78
79
  static ca_mv_runtime_type_t ca_mv_runtime_types[CA_MV_NUM_RUNTIME_OBJ_TYPES] = {
79
80
  { "CAFarray", "CA_OBJ_FARRAY", -1, Qnil, /* CA_MV_FARRAY */ 4 },
80
81
  { "CATranspose", "CA_OBJ_TRANSPOSE", -1, Qnil, /* CA_MV_TRANS */ 5 },
@@ -143,69 +144,68 @@ ca_mv_format_for (int8_t data_type)
143
144
  }
144
145
  }
145
146
 
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. */
147
+ /* The Ruby MemoryView specifier for one record member, written into `out`
148
+ with its byte order as ruby/memory_view.h writes it: a suffix ('<' / '>')
149
+ on an integer, its own letter on a float ('e' / 'E' little, 'g' / 'G'
150
+ big). A member without a declared byte order is in the machine's.
151
+ Returns 0 for a member the format has no item for: a nested struct, an
152
+ array member, :fixlen, :object, a bit member, or a complex member with a
153
+ declared byte order (Ruby has no complex specifier to suffix). */
184
154
  static int
185
- ca_mv_struct_member_resolve (VALUE info, const char **fmt_out, ca_size_t *bytes_out)
155
+ ca_mv_record_member_spec (VALUE info, char *out, size_t outlen,
156
+ ca_size_t *bytes_out)
186
157
  {
187
158
  VALUE type = rb_ary_entry(info, 1);
159
+ VALUE opts = rb_ary_entry(info, 2);
160
+ VALUE endian = Qnil;
161
+ const char *base;
162
+ int8_t dt;
188
163
  if (!SYMBOL_P(type)) return 0;
189
164
  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
165
  if (type_id == rb_intern("bitfield") ||
193
166
  type_id == rb_intern("fixlen") ||
194
167
  type_id == rb_intern("object")) {
195
168
  return 0;
196
169
  }
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;
170
+ dt = rb_ca_guess_type(rb_sym2str(type));
171
+ base = ca_mv_format_for(dt);
172
+ if (!base) return 0;
173
+ if (RB_TYPE_P(opts, T_HASH)) {
174
+ endian = rb_hash_aref(opts, ID2SYM(rb_intern("endian")));
175
+ }
176
+ if ( SYMBOL_P(endian) && ( SYM2ID(endian) == rb_intern("big")
177
+ || SYM2ID(endian) == rb_intern("little") ) ) {
178
+ int big = ( SYM2ID(endian) == rb_intern("big") );
179
+ switch (dt) {
180
+ case CA_FLOAT32: base = big ? "g" : "e"; break;
181
+ case CA_FLOAT64: base = big ? "G" : "E"; break;
182
+ case CA_CMPLX64: case CA_CMPLX128: return 0;
183
+ case CA_BOOLEAN: case CA_INT8: case CA_UINT8: break; /* one byte */
184
+ default:
185
+ snprintf(out, outlen, "%s%s", base, big ? ">" : "<");
186
+ *bytes_out = (ca_size_t) ca_sizeof[dt];
187
+ return 1;
188
+ }
189
+ }
190
+ snprintf(out, outlen, "%s", base);
201
191
  *bytes_out = (ca_size_t) ca_sizeof[dt];
202
192
  return 1;
203
193
  }
204
194
 
195
+ /* Build (or fetch from cache) the Ruby MemoryView format of a record: the
196
+ members' specifiers in their byte order in the record, with "x<N>" for
197
+ the padding between them and after the last. Ruby's format has no
198
+ member names; the record array gives them as #memory_view_field_names,
199
+ one per specifier. Returns Qnil for a record the format cannot describe:
200
+ a member it has no item for (see above), or members that overlap or come
201
+ out of offset order (a union).
202
+
203
+ The result is frozen and cached on the data_class, so the pointer
204
+ handed to a view stays valid while the class lives. */
205
205
  static VALUE
206
206
  ca_mv_struct_format_for_data_class (VALUE data_class)
207
207
  {
208
- ID iv = rb_intern("@__mv_struct_format__");
208
+ ID iv = rb_intern("@__mv_record_format__");
209
209
  if (rb_ivar_defined(data_class, iv)) {
210
210
  VALUE cached = rb_ivar_get(data_class, iv);
211
211
  /* Qfalse is a sentinel meaning "tried, found unsupported". */
@@ -216,61 +216,36 @@ ca_mv_struct_format_for_data_class (VALUE data_class)
216
216
  VALUE table = rb_const_get(data_class, rb_intern("MEMBER_TABLE"));
217
217
  long n = RARRAY_LEN(members);
218
218
  long i;
219
- VALUE buf = rb_str_buf_new(2 + 6 * n);
219
+ VALUE buf = rb_str_buf_new(4 * n + 8);
220
220
  ca_size_t cursor = 0;
221
221
  ca_size_t data_size = NUM2SIZE(rb_const_get(data_class, rb_intern("DATA_SIZE")));
222
-
223
- rb_str_buf_cat_ascii(buf, "T{");
222
+ char spec[16], pad[32];
224
223
 
225
224
  for (i = 0; i < n; i++) {
226
- VALUE name = rb_ary_entry(members, i);
227
- VALUE info = rb_hash_aref(table, name);
225
+ VALUE info = rb_hash_aref(table, rb_ary_entry(members, i));
226
+ ca_size_t offset, mb;
228
227
  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. */
228
+ offset = NUM2SIZE(rb_ary_entry(info, 0));
229
+ if (offset < cursor) goto unsupported;
230
+ if (!ca_mv_record_member_spec(info, spec, sizeof(spec), &mb)) goto unsupported;
241
231
  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;
232
+ snprintf(pad, sizeof(pad), "x%lld", (long long) (offset - cursor));
233
+ rb_str_buf_cat_ascii(buf, pad);
247
234
  }
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;
235
+ rb_str_buf_cat_ascii(buf, spec);
236
+ cursor = offset + mb;
255
237
  }
256
-
257
- /* Trailing padding (DATA_SIZE > last member's end). Elided form
258
- (see the alignment-gap branch above for rationale). */
238
+ if (cursor > data_size) goto unsupported;
259
239
  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);
240
+ snprintf(pad, sizeof(pad), "x%lld", (long long) (data_size - cursor));
241
+ rb_str_buf_cat_ascii(buf, pad);
264
242
  }
265
243
 
266
- rb_str_buf_cat_ascii(buf, "}");
267
244
  rb_obj_freeze(buf);
268
245
  rb_ivar_set(data_class, iv, buf);
269
246
  return buf;
270
247
 
271
248
  unsupported:
272
- /* Cache Qfalse to remember "we tried and gave up" — avoids
273
- re-walking MEMBER_TABLE on every wrap call. */
274
249
  rb_ivar_set(data_class, iv, Qfalse);
275
250
  return Qnil;
276
251
  }
@@ -282,7 +257,7 @@ ca_mv_data_type_exportable (int8_t data_type)
282
257
  }
283
258
 
284
259
  /* Same as ca_mv_data_type_exportable, but also accepts CA_FIXLEN:
285
- - with a data_class -> emit PEP 3118 "T{...}" struct format
260
+ - with a data_class -> emit the record's Ruby format (members in order)
286
261
  - without a data_class -> emit PEP 3118 "Ns" fixed-bytes format */
287
262
  static int
288
263
  ca_mv_ca_exportable (CArray *ca, VALUE obj)
@@ -424,35 +399,6 @@ ca_mv_carray_bytes_for (int8_t data_type, ssize_t item_size)
424
399
  : (ca_size_t) ca_sizeof[data_type];
425
400
  }
426
401
 
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
402
  /* ---------------- helpers ---------------- */
457
403
 
458
404
  static int
@@ -507,6 +453,8 @@ typedef struct {
507
453
  int depth; /* 0 = the view itself, 1 = parent, 2 = grand-, ... */
508
454
  const char *class_name; /* class name of the offending link */
509
455
  const char *reason; /* short explanation */
456
+ int face; /* refused because it is a Face: a copy is
457
+ refused too, so no copy is suggested */
510
458
  } ca_mv_reject_t;
511
459
 
512
460
  static bool ca_mv_stride_is_contig (CAStride *ca); /* defined below */
@@ -551,6 +499,18 @@ ca_mv_reject_reason_for (CArray *ca)
551
499
  return "this kind of view is not alias-capable";
552
500
  }
553
501
 
502
+ /* The entities whose bytes the producer exports: CArray, CAWrap and
503
+ CScalar. An entity installed from outside the core (ca_install_obj_type,
504
+ e.g. a CASource subclass) may hold its bytes elsewhere (cold at rest),
505
+ so neither it nor a view onto it is exported. */
506
+ static inline bool
507
+ ca_mv_is_core_entity (CArray *ca)
508
+ {
509
+ return ca->obj_type == CA_OBJ_ARRAY ||
510
+ ca->obj_type == CA_OBJ_ARRAY_WRAP ||
511
+ ca->obj_type == CA_OBJ_SCALAR;
512
+ }
513
+
554
514
  /* Resolve ca to an (entity, composed_strides, composed_base) triple
555
515
  suitable for zero-copy strided export. Uses ca_stride_compose_to_root
556
516
  to fold the CAStride parent chain (alias-eligible OR not, as long
@@ -569,15 +529,31 @@ ca_mv_compose_to_entity (CArray *ca, ca_mv_reject_t *rej,
569
529
  ca_size_t *out_strides,
570
530
  ca_size_t *out_base)
571
531
  {
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. */
532
+ rej->face = 0;
533
+ /* A Face is a 1:1 alias of its storage, so it is exported as that
534
+ storage -- but only a Face that registered its bytes as carrying its
535
+ values (ca_face_register_memory_view). The format comes from the Face
536
+ (data_class for a CARecord); the layout from the storage below. */
577
537
  if (ca_is_face(ca)) {
538
+ if (! ca_face_memory_view_exportable(ca)) {
539
+ rej->depth = 0;
540
+ rej->class_name = ca_mv_class_name_of(ca);
541
+ rej->reason = "a Face whose storage bytes do not carry its values "
542
+ "without the Face; export its storage instead "
543
+ "(e.g. .ticks, .codes)";
544
+ rej->face = 1;
545
+ return false;
546
+ }
578
547
  ca = ca_strip_face(ca);
579
548
  }
580
549
  if (ca_is_entity(ca)) {
550
+ if (! ca_mv_is_core_entity(ca)) {
551
+ rej->depth = 0;
552
+ rej->class_name = ca_mv_class_name_of(ca);
553
+ rej->reason = "installed outside the core; its bytes are not "
554
+ "exported";
555
+ return false;
556
+ }
581
557
  *out_entity = ca;
582
558
  *out_base = 0;
583
559
  return true;
@@ -601,6 +577,12 @@ ca_mv_compose_to_entity (CArray *ca, ca_mv_reject_t *rej,
601
577
  }
602
578
  return false;
603
579
  }
580
+ if (! ca_mv_is_core_entity(root)) {
581
+ rej->depth = -1;
582
+ rej->class_name = ca_mv_class_name_of(root);
583
+ rej->reason = "installed outside the core; its bytes are not exported";
584
+ return false;
585
+ }
604
586
  *out_entity = root;
605
587
  return true;
606
588
  }
@@ -641,30 +623,46 @@ ca_mv_available_p (VALUE obj)
641
623
  if (! ca_mv_extract(obj, &ca)) return false;
642
624
  if (! ca_mv_ca_exportable(ca, obj)) return false;
643
625
  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
626
+ /* The same resolver decides the export in ca_mv_get, so what this
627
+ says is what the export does. Wrap-via-MV exports are zero-copy:
628
+ require an alias chain to a core entity. Non-alias views (CASelect, CAFake, etc.) and CAStride
655
629
  chains broken by a non-contig link cannot be wrapped without a
656
630
  snapshot, which has wrong semantics for MV consumers. */
657
631
  return ca_mv_check_alias_chain(ca, &rej);
658
632
  }
659
633
 
634
+ /* "Ns" format for a plain CA_FIXLEN of N bytes. The Strings are kept in
635
+ a table keyed by N for the life of the process, so the pointer stays
636
+ valid for any view, and a frozen array (which cannot take an ivar) is
637
+ exported like any other. */
638
+ static VALUE ca_mv_fixlen_formats = Qnil;
639
+
640
+ static VALUE
641
+ ca_mv_fixlen_format (ca_size_t bytes)
642
+ {
643
+ VALUE key = SIZE2NUM(bytes);
644
+ VALUE fmt;
645
+ if ( NIL_P(ca_mv_fixlen_formats) ) {
646
+ ca_mv_fixlen_formats = rb_hash_new();
647
+ rb_gc_register_mark_object(ca_mv_fixlen_formats);
648
+ }
649
+ fmt = rb_hash_lookup(ca_mv_fixlen_formats, key);
650
+ if ( NIL_P(fmt) ) {
651
+ fmt = rb_obj_freeze(rb_sprintf("%lds", (long) bytes));
652
+ rb_hash_aset(ca_mv_fixlen_formats, key, fmt);
653
+ }
654
+ return fmt;
655
+ }
656
+
660
657
  static bool
661
658
  ca_mv_get (VALUE obj, rb_memory_view_t *view, int flags)
662
659
  {
663
660
  CArray *ca;
664
- ca_mv_strategy_t strategy;
661
+ CArray *storage;
665
662
  ssize_t *shape = NULL;
666
663
  ssize_t *strides = NULL;
667
664
  void *data_ptr = NULL;
665
+ const char *format;
668
666
  int writable_request;
669
667
 
670
668
  if (! ca_mv_extract(obj, &ca)) return false;
@@ -678,11 +676,10 @@ ca_mv_get (VALUE obj, rb_memory_view_t *view, int flags)
678
676
  Object#freeze on a subclass). */
679
677
  if (writable_request && OBJ_FROZEN(obj)) return false;
680
678
 
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). */
679
+ /* Resolve the chain to (entity, composed_strides, composed_base) with
680
+ the resolver available_p uses, so the two cannot disagree. The
681
+ layout is the storage's: a Face that got through is a 1:1 alias of
682
+ it. */
686
683
  CArray *resolved_entity = NULL;
687
684
  ca_size_t composed_strides[CA_RANK_MAX];
688
685
  ca_size_t composed_base = 0;
@@ -693,73 +690,66 @@ ca_mv_get (VALUE obj, rb_memory_view_t *view, int flags)
693
690
  return false;
694
691
  }
695
692
  }
693
+ storage = ca_is_face(ca) ? ca_strip_face(ca) : ca;
696
694
 
697
695
  /* SIMPLE consumer on non-contig view would need a snapshot -- not
698
696
  offered (consumers wanting a snapshot use from_memory_view). */
699
697
  {
700
698
  int wants_strides_early = (flags & RUBY_MEMORY_VIEW_STRIDES) ? 1 : 0;
701
699
  if (!wants_strides_early &&
702
- ca_mv_is_castride_family(ca) &&
703
- !ca_mv_stride_is_contig((CAStride *) ca)) {
700
+ ca_mv_is_castride_family(storage) &&
701
+ !ca_mv_stride_is_contig((CAStride *) storage)) {
704
702
  return false;
705
703
  }
706
704
  }
707
705
 
708
- strategy = ca_mv_strategy_for(ca->obj_type);
706
+ /* Format string, resolved before shape / strides are allocated since
707
+ building it can raise. Primitive types use the top-level table;
708
+ CA_FIXLEN + data_class emits the record's format, cached on the class,
709
+ so the pointer is stable until the class is GC'd. */
710
+ if (ca->data_type == CA_FIXLEN && RTEST(rb_ca_has_data_class(obj))) {
711
+ VALUE klass = rb_ca_data_class(obj);
712
+ VALUE fmt = ca_mv_struct_format_for_data_class(klass);
713
+ format = RSTRING_PTR(fmt);
714
+ }
715
+ else if (ca->data_type == CA_FIXLEN) {
716
+ /* Plain CA_FIXLEN (no data_class): PEP 3118 "Ns" fixed-bytes. */
717
+ format = RSTRING_PTR(ca_mv_fixlen_format(ca->bytes));
718
+ }
719
+ else {
720
+ format = ca_mv_format_for(ca->data_type);
721
+ }
709
722
 
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) {
723
+ if (ca_is_entity(storage)) {
724
+ /* Entity / wrap / scalar: its own buffer, always valid. No attach
725
+ needed. A CScalar is exported with ndim 0 and no shape, so none is
726
+ taken: release frees only what the view carries. */
727
+ data_ptr = storage->ptr;
728
+ if (ca->ndim >= 1 && storage->obj_type != CA_OBJ_SCALAR) {
716
729
  shape = ca_mv_alloc_shape(ca);
717
730
  strides = ca_mv_alloc_strides_contiguous(ca, shape, true);
718
731
  }
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: {
732
+ }
733
+ else {
734
+ /* A CAStride-family view (CARefer / CABlock / CAFarray / CATranspose
735
+ / CARepeat / CAField / plain CAStride) composed to an entity.
736
+ Expose `entity->ptr + composed_base` as data_ptr and
737
+ composed_strides as strides, both in entity byte space. The entity
738
+ owns its bytes (always valid via view->obj's parent chain while the
739
+ consumer holds the view).
740
+
741
+ CAREFUL: do not ca_attach the view here (nor ca_sync / ca_detach at
742
+ release). Exposure is alias-only: the consumer writes directly into
743
+ root bytes, so a sync would scatter bytes back to root as a no-op
744
+ chain -- and that chain triggers ca_update_mask / ca_sync(mask)
745
+ recursion that can raise on an invariant violation. Raising from a
746
+ release callback is fatal during GC sweep (newobj_of during sweep ->
747
+ BUG). */
741
748
  int8_t k;
742
749
  shape = ca_mv_alloc_shape(ca);
743
750
  strides = (ssize_t *) xmalloc(sizeof(ssize_t) * ca->ndim);
744
751
  for (k = 0; k < ca->ndim; k++) strides[k] = (ssize_t) composed_strides[k];
745
752
  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
753
  }
764
754
 
765
755
  view->obj = obj;
@@ -770,37 +760,11 @@ ca_mv_get (VALUE obj, rb_memory_view_t *view, int flags)
770
760
  views compute Σ (shape[k]-1)*|strides[k]| + item_size themselves. */
771
761
  view->byte_size = (ssize_t) ca_length(ca);
772
762
  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
- }
763
+ view->format = format;
800
764
  view->item_size = (ssize_t) ca->bytes;
801
765
  view->item_desc.components = NULL;
802
766
  view->item_desc.length = 0;
803
- view->ndim = (ca->obj_type == CA_OBJ_SCALAR) ? 0 : (ssize_t) ca->ndim;
767
+ view->ndim = (storage->obj_type == CA_OBJ_SCALAR) ? 0 : (ssize_t) ca->ndim;
804
768
  view->shape = (view->ndim == 0) ? NULL : shape;
805
769
  view->strides = (view->ndim == 0) ? NULL : strides;
806
770
  view->sub_offsets = NULL;
@@ -1075,6 +1039,25 @@ ca_mv_extract_type_and_mask_kwargs (int argc, VALUE *argv,
1075
1039
  return data_type;
1076
1040
  }
1077
1041
 
1042
+ /* ca_mv_acquire_and_validate as an rb_protect body. */
1043
+ typedef struct {
1044
+ VALUE src;
1045
+ rb_memory_view_t *view;
1046
+ int8_t explicit_data_type;
1047
+ int8_t *data_type;
1048
+ int8_t *ndim;
1049
+ ca_size_t *dim;
1050
+ } ca_mv_acquire_args_t;
1051
+
1052
+ static VALUE
1053
+ ca_mv_acquire_body (VALUE arg)
1054
+ {
1055
+ ca_mv_acquire_args_t *a = (ca_mv_acquire_args_t *) arg;
1056
+ ca_mv_acquire_and_validate(a->src, a->view, a->explicit_data_type,
1057
+ a->data_type, a->ndim, a->dim);
1058
+ return Qnil;
1059
+ }
1060
+
1078
1061
  /* Acquire and validate a mask MemoryView against the data side's shape.
1079
1062
  Returns an allocated holder (caller owns; release via the imported
1080
1063
  holder TypedData dfree path) with view active on success.
@@ -1255,8 +1238,21 @@ rb_ca_s_wrap_memory_view (int argc, VALUE *argv, VALUE klass)
1255
1238
  holder = ALLOC(ca_mv_imported_holder_t);
1256
1239
  holder->valid = false;
1257
1240
 
1258
- ca_mv_acquire_and_validate(obj, &holder->view, explicit_data_type,
1259
- &data_type, &ndim, dim);
1241
+ /* Acquisition raises on a producer it cannot take (and releases the
1242
+ view itself first); the holder is not a Ruby object yet, so it is
1243
+ freed here. */
1244
+ {
1245
+ ca_mv_acquire_args_t a;
1246
+ int tag = 0;
1247
+ a.src = obj; a.view = &holder->view;
1248
+ a.explicit_data_type = explicit_data_type;
1249
+ a.data_type = &data_type; a.ndim = &ndim; a.dim = dim;
1250
+ rb_protect(ca_mv_acquire_body, (VALUE) &a, &tag);
1251
+ if ( tag ) {
1252
+ xfree(holder);
1253
+ rb_jump_tag(tag);
1254
+ }
1255
+ }
1260
1256
  holder->valid = true;
1261
1257
  v = &holder->view;
1262
1258
 
@@ -1530,17 +1526,18 @@ rb_ca_s_memory_view_reject_reason (VALUE klass, VALUE obj)
1530
1526
  }
1531
1527
  if (! ca_mv_check_alias_chain(ca, &rej)) {
1532
1528
  const char *where;
1533
- if (rej.depth == 0) where = "this view";
1529
+ if (rej.depth < 0) where = "the root";
1530
+ else if (rej.depth == 0) where = "this view";
1534
1531
  else if (rej.depth == 1) where = "parent";
1535
1532
  else where = NULL; /* ancestor[N] */
1536
1533
  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);
1534
+ return rb_sprintf("%s is %s (%s); zero-copy wrap not possible.%s",
1535
+ where, rej.class_name, rej.reason,
1536
+ rej.face ? "" : " Export arr.copy for a snapshot.");
1540
1537
  } 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);
1538
+ return rb_sprintf("ancestor[%d] is %s (%s); zero-copy wrap not possible.%s",
1539
+ rej.depth - 1, rej.class_name, rej.reason,
1540
+ rej.face ? "" : " Export arr.copy for a snapshot.");
1544
1541
  }
1545
1542
  }
1546
1543
  return Qnil;
@@ -1566,8 +1563,8 @@ rb_ca_s_parse_memory_view_format (VALUE klass, VALUE fmt_v, VALUE item_size_v)
1566
1563
 
1567
1564
  /* Test hook: return the MV format string the producer would emit
1568
1565
  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. */
1566
+ by spec_ai to verify the record format emitted for CAStruct arrays.
1567
+ Returns nil if obj isn't exportable. */
1571
1568
  static VALUE
1572
1569
  rb_ca_s_memory_view_format (VALUE klass, VALUE obj)
1573
1570
  {
@@ -1578,24 +1575,30 @@ rb_ca_s_memory_view_format (VALUE klass, VALUE obj)
1578
1575
  return ca_mv_struct_format_for_data_class(rb_ca_data_class(obj));
1579
1576
  }
1580
1577
  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;
1578
+ /* Plain CA_FIXLEN: the same "Ns" String the producer (ca_mv_get)
1579
+ hands out via view->format. */
1580
+ return ca_mv_fixlen_format(ca->bytes);
1594
1581
  }
1595
1582
  const char *fmt = ca_mv_format_for(ca->data_type);
1596
1583
  return fmt ? rb_str_new_cstr(fmt) : Qnil;
1597
1584
  }
1598
1585
 
1586
+ /* CARecord#memory_view_field_names: the names of the record's members,
1587
+ one per specifier of the record's MemoryView format, in that order.
1588
+ Ruby's format has no place for them, so a bridge that writes a format
1589
+ which does (PEP 3118 "T{...}") asks the exporting object for them under
1590
+ this name. nil when the record has no MemoryView format. */
1591
+ static VALUE
1592
+ rb_ca_record_memory_view_field_names (VALUE self)
1593
+ {
1594
+ VALUE data_class = rb_ca_data_class(self);
1595
+ if ( NIL_P(data_class)
1596
+ || NIL_P(ca_mv_struct_format_for_data_class(data_class)) ) {
1597
+ return Qnil;
1598
+ }
1599
+ return rb_ary_dup(rb_const_get(data_class, rb_intern("MEMBERS")));
1600
+ }
1601
+
1599
1602
  /* ---------------- Init ---------------- */
1600
1603
 
1601
1604
  static const rb_memory_view_entry_t ca_memory_view_entry = {
@@ -1647,4 +1650,7 @@ Init_carray_memory_view (void)
1647
1650
  rb_ca_s_parse_memory_view_format, 2);
1648
1651
  rb_define_singleton_method(rb_cCArray, "__memory_view_format__",
1649
1652
  rb_ca_s_memory_view_format, 1);
1653
+ rb_define_method(rb_const_get(rb_cObject, rb_intern("CARecord")),
1654
+ "memory_view_field_names",
1655
+ rb_ca_record_memory_view_field_names, 0);
1650
1656
  }