carray 2.0.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (339) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +5 -25
  3. data/CHANGELOG.md +16 -0
  4. data/LICENSE +1 -1
  5. data/NEWS.md +3 -0
  6. data/README.md +128 -44
  7. data/carray.gemspec +22 -24
  8. data/ext/ca_array_pool.c +91 -0
  9. data/ext/ca_axis_descriptor.h +186 -0
  10. data/ext/ca_axis_dispatch.c +924 -0
  11. data/ext/ca_axis_group.c +1208 -0
  12. data/ext/ca_bincmp_dispatch.c +76 -0
  13. data/ext/ca_bincmp_dispatch.h +85 -0
  14. data/ext/ca_binop_dispatch.c +125 -0
  15. data/ext/ca_binop_dispatch.h +159 -0
  16. data/ext/ca_categorical_iterator.c +1375 -0
  17. data/ext/ca_compare.c +94 -0
  18. data/ext/ca_compare.h +26 -0
  19. data/ext/ca_composite_dispatch.c +414 -0
  20. data/ext/ca_composite_dispatch.h +116 -0
  21. data/ext/ca_for_buffer.h +96 -0
  22. data/ext/ca_for_each_element.h +241 -0
  23. data/ext/ca_group_iter.c +304 -0
  24. data/ext/ca_iter_substrate.h +325 -0
  25. data/ext/ca_kernel_iterator.c +4321 -0
  26. data/ext/ca_kernel_iterator.h +2603 -0
  27. data/ext/ca_moncmp_dispatch.c +37 -0
  28. data/ext/ca_moncmp_dispatch.h +62 -0
  29. data/ext/ca_monop_dispatch.c +200 -0
  30. data/ext/ca_monop_dispatch.h +235 -0
  31. data/ext/ca_obj_array.c +355 -359
  32. data/ext/ca_obj_bincmp.c +809 -0
  33. data/ext/ca_obj_binop.c +892 -0
  34. data/ext/ca_obj_bitarray.c +369 -164
  35. data/ext/ca_obj_bitfield.c +294 -234
  36. data/ext/ca_obj_block.c +189 -711
  37. data/ext/ca_obj_byte_swap.c +766 -0
  38. data/ext/ca_obj_const_string.c +965 -0
  39. data/ext/ca_obj_face.c +670 -0
  40. data/ext/ca_obj_face.h +247 -0
  41. data/ext/ca_obj_fake.c +228 -100
  42. data/ext/ca_obj_farray.c +54 -441
  43. data/ext/ca_obj_field.c +82 -529
  44. data/ext/ca_obj_fixlen_string.c +306 -0
  45. data/ext/ca_obj_grid.c +858 -440
  46. data/ext/ca_obj_meld.c +1034 -0
  47. data/ext/ca_obj_moncmp.c +569 -0
  48. data/ext/ca_obj_monop.c +1111 -0
  49. data/ext/ca_obj_object.c +774 -298
  50. data/ext/ca_obj_record.c +468 -0
  51. data/ext/ca_obj_reduce.c +97 -82
  52. data/ext/ca_obj_refer.c +569 -459
  53. data/ext/ca_obj_remap.c +475 -0
  54. data/ext/ca_obj_repeat.c +92 -477
  55. data/ext/ca_obj_roll.c +616 -0
  56. data/ext/ca_obj_select.c +344 -296
  57. data/ext/ca_obj_select_axis.c +1296 -0
  58. data/ext/ca_obj_shift.c +230 -792
  59. data/ext/ca_obj_source.c +78 -0
  60. data/ext/ca_obj_stack.c +1173 -0
  61. data/ext/ca_obj_stride.c +2501 -0
  62. data/ext/ca_obj_string.c +268 -0
  63. data/ext/ca_obj_tile.c +614 -0
  64. data/ext/ca_obj_time.c +546 -0
  65. data/ext/ca_obj_timedelta.c +435 -0
  66. data/ext/ca_obj_transpose.c +62 -516
  67. data/ext/ca_obj_triop.c +746 -0
  68. data/ext/ca_obj_unbound_repeat.c +208 -241
  69. data/ext/ca_obj_window.c +1131 -563
  70. data/ext/ca_op_byte_swap.c +175 -0
  71. data/ext/ca_op_ipower.c +319 -0
  72. data/ext/ca_op_powi.h +88 -0
  73. data/ext/ca_sort_kernels.h +132 -0
  74. data/ext/ca_sweep_engine.c +430 -0
  75. data/ext/ca_sweep_engine.h +157 -0
  76. data/ext/ca_transform_common.c +228 -0
  77. data/ext/ca_triop_dispatch.c +55 -0
  78. data/ext/ca_triop_dispatch.h +62 -0
  79. data/ext/carray.h +795 -402
  80. data/ext/carray_access.c +831 -711
  81. data/ext/carray_attribute.c +98 -330
  82. data/ext/carray_bincount.c +255 -0
  83. data/ext/carray_broadcast.c +283 -0
  84. data/ext/carray_call_cfunc.c +1360 -828
  85. data/ext/carray_call_cfunc.h +160 -0
  86. data/ext/carray_cast.c +1212 -301
  87. data/ext/carray_cast_func.rb +81 -40
  88. data/ext/carray_class.c +53 -63
  89. data/ext/carray_config.h +28 -0
  90. data/ext/carray_conversion.c +350 -346
  91. data/ext/carray_copy.c +156 -268
  92. data/ext/carray_core.c +1342 -199
  93. data/ext/carray_count.c +312 -0
  94. data/ext/carray_data_type.c +43 -19
  95. data/ext/carray_element.c +585 -213
  96. data/ext/carray_factorize.c +2542 -0
  97. data/ext/carray_generate.c +230 -559
  98. data/ext/carray_histogram.c +490 -0
  99. data/ext/carray_hold.c +228 -0
  100. data/ext/carray_index_classifier.c +1035 -0
  101. data/ext/carray_index_classifier.h +27 -0
  102. data/ext/carray_internal.h +120 -0
  103. data/ext/carray_kernels_bincmp.c +4445 -0
  104. data/ext/carray_kernels_binop.c +10979 -0
  105. data/ext/carray_kernels_init.c +36 -0
  106. data/ext/carray_kernels_map.c +3466 -0
  107. data/ext/carray_kernels_moncmp.c +2096 -0
  108. data/ext/carray_kernels_monop.c +18312 -0
  109. data/ext/carray_kernels_reduce_aggregate.c +25836 -0
  110. data/ext/carray_kernels_reduce_boolean.c +329 -0
  111. data/ext/carray_kernels_reduce_cumulative.c +14592 -0
  112. data/ext/carray_kernels_reduce_extreme.c +16947 -0
  113. data/ext/carray_kernels_reduce_variance.c +3909 -0
  114. data/ext/carray_kernels_scan.c +3692 -0
  115. data/ext/carray_kernels_search.c +32137 -0
  116. data/ext/carray_kernels_sort.c +10625 -0
  117. data/ext/carray_kernels_triop.c +1391 -0
  118. data/ext/carray_lazy.c +567 -0
  119. data/ext/carray_loop.c +88 -200
  120. data/ext/carray_mask.c +848 -154
  121. data/ext/carray_math_kernel.h +120 -0
  122. data/ext/carray_mathfunc.c +10 -241
  123. data/ext/carray_median_percentile.c +1257 -0
  124. data/ext/carray_memory_view.c +1625 -0
  125. data/ext/carray_operator.c +1526 -318
  126. data/ext/carray_order.c +664 -1394
  127. data/ext/carray_partition.c +416 -0
  128. data/ext/carray_random.c +518 -0
  129. data/ext/carray_scatter.c +357 -0
  130. data/ext/carray_slab.c +1219 -0
  131. data/ext/carray_slab.h +84 -0
  132. data/ext/carray_sort.c +829 -0
  133. data/ext/carray_sort_kernel.c +620 -0
  134. data/ext/carray_struct.c +695 -0
  135. data/ext/carray_test.c +343 -229
  136. data/ext/carray_undef.c +34 -17
  137. data/ext/carray_utils.c +175 -74
  138. data/ext/extconf.rb +216 -55
  139. data/ext/mk_call_cfunc.rb +480 -0
  140. data/ext/mkkernel.rb +8842 -0
  141. data/ext/ruby_carray.c +202 -101
  142. data/ext/version.h +4 -14
  143. data/ext/version.rb +5 -13
  144. data/lib/carray/arrow_tensor.rb +401 -0
  145. data/lib/carray/attribute.rb +166 -0
  146. data/lib/carray/autoload_carray.rb +220 -0
  147. data/lib/carray/autoload_method_extension.rb +44 -0
  148. data/lib/carray/axis_group.rb +711 -0
  149. data/lib/carray/basics.rb +481 -0
  150. data/lib/carray/bincount_nd.rb +358 -0
  151. data/lib/carray/block_iterator.rb +604 -0
  152. data/lib/carray/boolean_reduce.rb +109 -0
  153. data/lib/carray/categorical.rb +561 -0
  154. data/lib/carray/categorical_iterator.rb +1062 -0
  155. data/lib/carray/complex.rb +150 -0
  156. data/lib/carray/conditional.rb +216 -0
  157. data/lib/carray/const_string.rb +228 -0
  158. data/lib/carray/construct.rb +139 -328
  159. data/lib/carray/core_extensions.rb +240 -0
  160. data/lib/carray/data_type_extension.rb +233 -0
  161. data/lib/carray/fixlen_string.rb +95 -0
  162. data/lib/carray/frame/concat.rb +132 -0
  163. data/lib/carray/frame/convert.rb +95 -0
  164. data/lib/carray/frame/csv_parser.rb +211 -0
  165. data/lib/carray/frame/frame.rb +649 -0
  166. data/lib/carray/frame/group.rb +186 -0
  167. data/lib/carray/frame/io.rb +164 -0
  168. data/lib/carray/frame/join.rb +248 -0
  169. data/lib/carray/frame/records.rb +99 -0
  170. data/lib/carray/frame/sort.rb +113 -0
  171. data/lib/carray/frame/verbs.rb +299 -0
  172. data/lib/carray/frame.rb +16 -0
  173. data/lib/carray/histogram.rb +512 -0
  174. data/lib/carray/inspect.rb +37 -20
  175. data/lib/carray/iterator.rb +57 -349
  176. data/lib/carray/lazy.rb +889 -0
  177. data/lib/carray/mask_gap_fill.rb +200 -0
  178. data/lib/carray/math.rb +78 -342
  179. data/lib/carray/meld_reduce.rb +289 -0
  180. data/lib/carray/methods/align_addr.rb +116 -0
  181. data/lib/carray/methods/bin.rb +128 -0
  182. data/lib/carray/methods/bincount.rb +87 -0
  183. data/lib/carray/methods/bit_string.rb +92 -0
  184. data/lib/carray/methods/broadcast.rb +63 -0
  185. data/lib/carray/methods/choose.rb +39 -0
  186. data/lib/carray/methods/composition.rb +280 -0
  187. data/lib/carray/methods/gather_nd.rb +206 -0
  188. data/lib/carray/methods/index.rb +39 -0
  189. data/lib/carray/methods/insert_block.rb +99 -0
  190. data/lib/carray/methods/is_in.rb +141 -0
  191. data/lib/carray/methods/join.rb +90 -0
  192. data/lib/carray/methods/locate_addr.rb +47 -0
  193. data/lib/carray/methods/mask_duplicates.rb +41 -0
  194. data/lib/carray/methods/meshgrid.rb +91 -0
  195. data/lib/carray/methods/mode.rb +126 -0
  196. data/lib/carray/methods/nunique.rb +46 -0
  197. data/lib/carray/methods/resize.rb +56 -0
  198. data/lib/carray/methods/snap.rb +156 -0
  199. data/lib/carray/methods/string_format.rb +57 -0
  200. data/lib/carray/methods/unique.rb +47 -0
  201. data/lib/carray/methods/value_counts.rb +71 -0
  202. data/lib/carray/mkmf.rb +124 -101
  203. data/lib/carray/runtime.rb +108 -0
  204. data/lib/carray/serialize.rb +478 -167
  205. data/lib/carray/slab_iterator.rb +292 -0
  206. data/lib/carray/stack.rb +291 -0
  207. data/lib/carray/string.rb +56 -180
  208. data/lib/carray/string_operation_extension.rb +289 -0
  209. data/lib/carray/struct.rb +335 -323
  210. data/lib/carray/struct_builder.rb +697 -0
  211. data/lib/carray/table.rb +41 -2
  212. data/lib/carray/time.rb +2255 -38
  213. data/lib/carray/window_iterator.rb +655 -0
  214. data/lib/carray.rb +55 -57
  215. metadata +163 -130
  216. data/Rakefile +0 -51
  217. data/TODO.md +0 -18
  218. data/ext/ca_iter_block.c +0 -257
  219. data/ext/ca_iter_dimension.c +0 -299
  220. data/ext/ca_iter_window.c +0 -214
  221. data/ext/ca_obj_mapping.c +0 -644
  222. data/ext/carray_iterator.c +0 -641
  223. data/ext/carray_math.rb +0 -850
  224. data/ext/carray_numeric.c +0 -259
  225. data/ext/carray_sort_addr.c +0 -254
  226. data/ext/carray_stat.c +0 -2100
  227. data/ext/carray_stat_proc.rb +0 -1999
  228. data/ext/mkmath.rb +0 -741
  229. data/ext/ruby_ccomplex.c +0 -509
  230. data/ext/ruby_float_func.c +0 -86
  231. data/lib/carray/array.rb +0 -8
  232. data/lib/carray/autoload/autoload_base.rb +0 -19
  233. data/lib/carray/autoload/autoload_gem_cairo.rb +0 -9
  234. data/lib/carray/autoload/autoload_gem_ffi.rb +0 -9
  235. data/lib/carray/autoload/autoload_gem_gnuplot.rb +0 -2
  236. data/lib/carray/autoload/autoload_gem_io_csv.rb +0 -14
  237. data/lib/carray/autoload/autoload_gem_io_pg.rb +0 -6
  238. data/lib/carray/autoload/autoload_gem_io_sqlite3.rb +0 -12
  239. data/lib/carray/autoload/autoload_gem_narray.rb +0 -10
  240. data/lib/carray/autoload/autoload_gem_numo_narray.rb +0 -15
  241. data/lib/carray/autoload/autoload_gem_opencv.rb +0 -16
  242. data/lib/carray/autoload/autoload_gem_random.rb +0 -8
  243. data/lib/carray/autoload/autoload_gem_rmagick.rb +0 -23
  244. data/lib/carray/autoload/autoload_gem_zimg.rb +0 -3
  245. data/lib/carray/autoload/autoload_io_imagemagick.rb +0 -6
  246. data/lib/carray/autoload/autoload_math_histogram.rb +0 -5
  247. data/lib/carray/autoload/autoload_math_recurrence.rb +0 -6
  248. data/lib/carray/autoload/autoload_object_iterator.rb +0 -1
  249. data/lib/carray/autoload/autoload_object_link.rb +0 -1
  250. data/lib/carray/autoload/autoload_object_pack.rb +0 -2
  251. data/lib/carray/autoload.rb +0 -141
  252. data/lib/carray/basic.rb +0 -191
  253. data/lib/carray/broadcast.rb +0 -101
  254. data/lib/carray/compose.rb +0 -315
  255. data/lib/carray/convert.rb +0 -115
  256. data/lib/carray/info.rb +0 -110
  257. data/lib/carray/io/imagemagick.rb +0 -235
  258. data/lib/carray/mask.rb +0 -102
  259. data/lib/carray/math/histogram.rb +0 -177
  260. data/lib/carray/math/recurrence.rb +0 -93
  261. data/lib/carray/object/ca_obj_iterator.rb +0 -50
  262. data/lib/carray/object/ca_obj_link.rb +0 -50
  263. data/lib/carray/object/ca_obj_pack.rb +0 -99
  264. data/lib/carray/obsolete.rb +0 -256
  265. data/lib/carray/ordering.rb +0 -181
  266. data/lib/carray/testing.rb +0 -51
  267. data/lib/carray/transform.rb +0 -109
  268. data/misc/Methods.ja.md +0 -182
  269. data/misc/NOTE +0 -51
  270. data/spec/Classes/CABitfield_spec.rb +0 -58
  271. data/spec/Classes/CABlockIterator_spec.rb +0 -114
  272. data/spec/Classes/CABlock_spec.rb +0 -205
  273. data/spec/Classes/CAField_spec.rb +0 -39
  274. data/spec/Classes/CAGrid_spec.rb +0 -75
  275. data/spec/Classes/CAMap_spec.rb +0 -0
  276. data/spec/Classes/CAMapping_spec.rb +0 -105
  277. data/spec/Classes/CAObject_attribute_spec.rb +0 -33
  278. data/spec/Classes/CAObject_spec.rb +0 -33
  279. data/spec/Classes/CARefer_spec.rb +0 -93
  280. data/spec/Classes/CARepeat_spec.rb +0 -65
  281. data/spec/Classes/CASelect_spec.rb +0 -22
  282. data/spec/Classes/CAShift_spec.rb +0 -16
  283. data/spec/Classes/CAStruct_spec.rb +0 -71
  284. data/spec/Classes/CATranspose_spec.rb +0 -60
  285. data/spec/Classes/CAUnboudRepeat_spec.rb +0 -102
  286. data/spec/Classes/CAWindow_spec.rb +0 -54
  287. data/spec/Classes/CAWrap_spec.rb +0 -8
  288. data/spec/Classes/CArray_spec.rb +0 -184
  289. data/spec/Classes/CScalar_spec.rb +0 -55
  290. data/spec/Classes/ex1.rb +0 -46
  291. data/spec/Features/feature_130_spec.rb +0 -19
  292. data/spec/Features/feature_attributes_spec.rb +0 -280
  293. data/spec/Features/feature_boolean_spec.rb +0 -98
  294. data/spec/Features/feature_broadcast.rb +0 -116
  295. data/spec/Features/feature_cast_function.rb +0 -19
  296. data/spec/Features/feature_cast_spec.rb +0 -33
  297. data/spec/Features/feature_class_spec.rb +0 -84
  298. data/spec/Features/feature_complex_spec.rb +0 -42
  299. data/spec/Features/feature_composite_spec.rb +0 -124
  300. data/spec/Features/feature_convert_spec.rb +0 -46
  301. data/spec/Features/feature_copy_spec.rb +0 -123
  302. data/spec/Features/feature_creation_spec.rb +0 -84
  303. data/spec/Features/feature_element_spec.rb +0 -144
  304. data/spec/Features/feature_extream_spec.rb +0 -54
  305. data/spec/Features/feature_generate_spec.rb +0 -74
  306. data/spec/Features/feature_index_spec.rb +0 -69
  307. data/spec/Features/feature_mask_spec.rb +0 -580
  308. data/spec/Features/feature_math_spec.rb +0 -97
  309. data/spec/Features/feature_order_spec.rb +0 -146
  310. data/spec/Features/feature_ref_store_spec.rb +0 -209
  311. data/spec/Features/feature_serialization_spec.rb +0 -125
  312. data/spec/Features/feature_stat_spec.rb +0 -397
  313. data/spec/Features/feature_virtual_spec.rb +0 -48
  314. data/spec/Features/method_eq_spec.rb +0 -81
  315. data/spec/Features/method_is_nan_spec.rb +0 -12
  316. data/spec/Features/method_map_spec.rb +0 -54
  317. data/spec/Features/method_max_with.rb +0 -20
  318. data/spec/Features/method_min_with.rb +0 -19
  319. data/spec/Features/method_ne_spec.rb +0 -18
  320. data/spec/Features/method_project_spec.rb +0 -188
  321. data/spec/Features/method_ref_spec.rb +0 -27
  322. data/spec/Features/method_round_spec.rb +0 -11
  323. data/spec/Features/method_s_linspace_spec.rb +0 -48
  324. data/spec/Features/method_s_span_spec.rb +0 -14
  325. data/spec/Features/method_seq_spec.rb +0 -47
  326. data/spec/Features/method_sort_with.rb +0 -43
  327. data/spec/Features/method_sorted_with.rb +0 -29
  328. data/spec/Features/method_span_spec.rb +0 -42
  329. data/spec/Features/method_wrap_readonly_spec.rb +0 -43
  330. data/spec/UnitTest/test_CAVirtual.rb +0 -214
  331. data/spec/spec_all.rb +0 -10
  332. data/utils/ca_ase.rb +0 -21
  333. data/utils/ca_methods.rb +0 -15
  334. data/utils/cast_checker.rb +0 -30
  335. data/utils/convert_test.rb +0 -73
  336. data/utils/extract_yard.rb +0 -22
  337. data/utils/guess_shape.rb +0 -76
  338. data/utils/monkey_patch_methods.rb +0 -62
  339. data/utils/remove_resource_fork.sh +0 -5
@@ -0,0 +1,1625 @@
1
+ /* ---------------------------------------------------------------------------
2
+
3
+ Ruby MemoryView protocol adapter for CArray.
4
+
5
+ Exporter (zero-copy, alias-only producer):
6
+ - entity arrays (CArray, CScalar, CAWrap): direct, contiguous
7
+ - CARefer / CABlock / CAFarray / CATranspose / CAField / plain
8
+ CAStride: zero-copy strided (composed to the root entity)
9
+ - CARepeat: zero-copy via stride=0 on repeated axes
10
+ - non-alias views (CASelect, CAGrid, CAShift, CAWindow, CAFake,
11
+ CAReduce, CABitarray, CABitfield, CAObject): rejected by the
12
+ producer; use CArray.from_memory_view for a snapshot copy
13
+
14
+ Importer:
15
+ - CArray.from_memory_view (copy; accepts strided)
16
+ - CArray.wrap_memory_view (zero-copy; contiguous only)
17
+
18
+ Format-string contract:
19
+ - Producer emits PEP 3118 strict; consumer is Postel-permissive
20
+ (accepts synonyms other producers emit).
21
+ - See docs/interop/MemoryViewFormat.md for the format-string contract.
22
+ - See devel/DESIGN_MemoryView.md for the design rationale.
23
+ - See docs/interop/MemoryView.md for the user-facing MV API.
24
+
25
+ ---------------------------------------------------------------------------- */
26
+
27
+ #include "carray.h"
28
+ #include "ca_obj_face.h" /* ca_strip_face for Face producers */
29
+ #include "ruby/memory_view.h"
30
+ #include <string.h>
31
+
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. */
34
+
35
+ /* ---------------- forward declarations for non-public structs ----------------
36
+ CABlock, CARefer, CARepeat are declared in carray.h. CAFarray
37
+ and CATranspose are CAStride subclasses -- this file reads
38
+ CAStride's strides[]/base_offset directly for them. */
39
+
40
+ /* ---------------- runtime-assigned obj_type ids ---------------- */
41
+
42
+ #define CA_MV_NUM_RUNTIME_OBJ_TYPES 11
43
+
44
+ typedef struct {
45
+ const char *class_name;
46
+ const char *const_name;
47
+ int id; /* -1 until resolved at Init */
48
+ VALUE klass; /* Qnil until resolved at Init */
49
+ int strategy; /* one of ca_mv_strategy_t (filled in lookup table) */
50
+ } ca_mv_runtime_type_t;
51
+
52
+ /* Set strategy via numeric constants below; ca_mv_strategy_t enum is
53
+ forward-declared. We can't put a forward-declared enum into a static
54
+ initialiser, so use a parallel integer table. */
55
+
56
+ /* ---------------- obj_type strategy ---------------- */
57
+
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. */
64
+ typedef enum {
65
+ CA_MV_REJECT,
66
+ CA_MV_DIRECT, /* entity / wrap / scalar; ca->ptr valid as-is */
67
+ CA_MV_REFER, /* CARefer; alias via compose-to-root */
68
+ CA_MV_BLOCK, /* CABlock; alias with strides via compose-to-root */
69
+ CA_MV_FARRAY, /* CAFarray; alias with column-major strides */
70
+ CA_MV_TRANS, /* CATranspose; alias with permuted strides */
71
+ CA_MV_REPEAT, /* CARepeat; alias with stride=0 on repeated dims */
72
+ CA_MV_ATTACH, /* unreachable: rejected by alias-chain check */
73
+ } ca_mv_strategy_t;
74
+
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. */
78
+ static ca_mv_runtime_type_t ca_mv_runtime_types[CA_MV_NUM_RUNTIME_OBJ_TYPES] = {
79
+ { "CAFarray", "CA_OBJ_FARRAY", -1, Qnil, /* CA_MV_FARRAY */ 4 },
80
+ { "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
+ { "CAGrid", "CA_OBJ_GRID", -1, Qnil, /* CA_MV_ATTACH */ 7 },
84
+ { "CAShift", "CA_OBJ_SHIFT", -1, Qnil, /* CA_MV_ATTACH */ 7 },
85
+ { "CAWindow", "CA_OBJ_WINDOW", -1, Qnil, /* CA_MV_ATTACH */ 7 },
86
+ { "CAFake", "CA_OBJ_FAKE", -1, Qnil, /* CA_MV_ATTACH */ 7 },
87
+ /* CAField is a CAStride subclass: zero-copy strided export via
88
+ parent->ptr + base_offset, no materialise. */
89
+ { "CAField", "CA_OBJ_FIELD", -1, Qnil, /* CA_MV_BLOCK */ 3 },
90
+ { "CAReduce", "CA_OBJ_REDUCE", -1, Qnil, /* CA_MV_ATTACH */ 7 },
91
+ { "CABitarray", "CA_OBJ_BITARRAY", -1, Qnil, /* CA_MV_REJECT */ 0 },
92
+ { "CABitfield", "CA_OBJ_BITFIELD", -1, Qnil, /* CA_MV_REJECT */ 0 },
93
+ /* CARecord: composite Face wrapping a CA_FIXLEN entity 1:1
94
+ (ca->ptr aliases parent->ptr). ATTACH strategy
95
+ is the conservative choice — same as CAFake; storage layout is
96
+ identical to parent so direct export would also work in principle. */
97
+ { "CARecord", "CA_OBJ_RECORD", -1, Qnil, /* CA_MV_ATTACH */ 7 },
98
+ };
99
+
100
+ /* ---------------- format / data_type mapping ----------------
101
+ Bidirectional table. Outbound table (ca_mv_format_for) picks the
102
+ canonical PEP 3118 specifier for each CArray data_type. Inbound
103
+ table (ca_mv_data_type_from_format) is Postel: it accepts a wider
104
+ set of synonyms that other producers emit (they describe int32
105
+ differently). */
106
+
107
+ static const char *
108
+ ca_mv_format_for (int8_t data_type)
109
+ {
110
+ switch (data_type) {
111
+ case CA_BOOLEAN: return "?";
112
+ case CA_INT8: return "b";
113
+ case CA_UINT8: return "B";
114
+ case CA_INT16: return "h";
115
+ case CA_UINT16: return "H";
116
+ case CA_INT32: return "i";
117
+ case CA_UINT32: return "I";
118
+ case CA_INT64: return "q";
119
+ case CA_UINT64: return "Q";
120
+ case CA_FLOAT32: return "f";
121
+ case CA_FLOAT64: return "d";
122
+ case CA_CMPLX64: return "Zf";
123
+ case CA_CMPLX128: return "Zd";
124
+ default:
125
+ return NULL;
126
+ }
127
+ }
128
+
129
+ /* PEP 3118 single-character specifier for a primitive data_type, for
130
+ use inside a `T{...}` struct format body. Identical to the top-level
131
+ producer specifier: since the 2026-06-29 PEP 3118 strict flip both the
132
+ top-level and struct-body contexts emit the same chars, so this
133
+ delegates to ca_mv_format_for. Kept as a distinct entry point so the
134
+ struct-body context can diverge again without touching call sites.
135
+
136
+ Returns NULL for non-primitive (CA_FIXLEN / CA_OBJECT) or for the
137
+ retired float128 / cmplx256 enum slots. */
138
+ static const char *
139
+ ca_mv_pep3118_char (int8_t data_type)
140
+ {
141
+ return ca_mv_format_for(data_type);
142
+ }
143
+
144
+ /* Build (or fetch from cache) the PEP 3118 struct format string for
145
+ a data_class. Walks MEMBER_TABLE in MEMBERS order and emits
146
+ "T{<fmt>:<name>:<fmt>:<name>:}". Returns Qnil if the class
147
+ contains a member type the producer cannot express (nested
148
+ sub-struct class, CArray template, :fixlen, :object).
149
+
150
+ The result is frozen and cached on the data_class via
151
+ @__mv_struct_format__ so successive calls are O(1) and the
152
+ underlying RSTRING_PTR is stable (the cached String is rooted by
153
+ the class, which outlives any MV produced from it). */
154
+ /* Helper: resolve a MEMBER_TABLE member-type slot to (fmt, bytes).
155
+ Returns 1 on success, 0 if the type is not expressible in PEP 3118
156
+ struct format (nested class, CArray template, :fixlen, :object,
157
+ :bitfield). Does NOT raise — :bitfield in particular is checked
158
+ before rb_ca_guess_type so the struct-format walk can give up
159
+ gracefully on bit-bearing structs. */
160
+ static int
161
+ ca_mv_struct_member_resolve (VALUE info, const char **fmt_out, ca_size_t *bytes_out)
162
+ {
163
+ VALUE type = rb_ary_entry(info, 1);
164
+ if (!SYMBOL_P(type)) return 0;
165
+ ID type_id = SYM2ID(type);
166
+ /* Reject explicitly unsupported symbol types up front (so
167
+ rb_ca_guess_type never sees them and never raises). */
168
+ if (type_id == rb_intern("bitfield") ||
169
+ type_id == rb_intern("fixlen") ||
170
+ type_id == rb_intern("object")) {
171
+ return 0;
172
+ }
173
+ int8_t dt = rb_ca_guess_type(rb_sym2str(type));
174
+ const char *fmt = ca_mv_pep3118_char(dt);
175
+ if (!fmt) return 0;
176
+ *fmt_out = fmt;
177
+ *bytes_out = (ca_size_t) ca_sizeof[dt];
178
+ return 1;
179
+ }
180
+
181
+ static VALUE
182
+ ca_mv_struct_format_for_data_class (VALUE data_class)
183
+ {
184
+ ID iv = rb_intern("@__mv_struct_format__");
185
+ if (rb_ivar_defined(data_class, iv)) {
186
+ VALUE cached = rb_ivar_get(data_class, iv);
187
+ /* Qfalse is a sentinel meaning "tried, found unsupported". */
188
+ return (cached == Qfalse) ? Qnil : cached;
189
+ }
190
+
191
+ VALUE members = rb_const_get(data_class, rb_intern("MEMBERS"));
192
+ VALUE table = rb_const_get(data_class, rb_intern("MEMBER_TABLE"));
193
+ long n = RARRAY_LEN(members);
194
+ long i;
195
+ VALUE buf = rb_str_buf_new(2 + 6 * n);
196
+ ca_size_t cursor = 0;
197
+ ca_size_t data_size = NUM2SIZE(rb_const_get(data_class, rb_intern("DATA_SIZE")));
198
+
199
+ rb_str_buf_cat_ascii(buf, "T{");
200
+
201
+ for (i = 0; i < n; i++) {
202
+ VALUE name = rb_ary_entry(members, i);
203
+ VALUE info = rb_hash_aref(table, name);
204
+ if (NIL_P(info)) goto unsupported;
205
+ ca_size_t offset = NUM2SIZE(rb_ary_entry(info, 0));
206
+
207
+ const char *fmt;
208
+ ca_size_t mb;
209
+ if (!ca_mv_struct_member_resolve(info, &fmt, &mb)) goto unsupported;
210
+
211
+ /* If alignment or explicit padding has left a gap before this
212
+ member, emit "<N>x:" to account for it. PEP 3118 uses 'x'
213
+ as the pad-byte specifier. The canonical form elides the name
214
+ slot after the pad spec (padding is anonymous by definition);
215
+ consumers MUST accept both elided and named (legacy "<N>x:_:")
216
+ forms. */
217
+ if (offset > cursor) {
218
+ char pad_decl[32];
219
+ snprintf(pad_decl, sizeof(pad_decl), "%lldx:",
220
+ (long long) (offset - cursor));
221
+ rb_str_buf_cat_ascii(buf, pad_decl);
222
+ cursor = offset;
223
+ }
224
+
225
+ rb_str_buf_cat_ascii(buf, fmt);
226
+ rb_str_buf_cat_ascii(buf, ":");
227
+ /* MEMBERS entries are strings; emit verbatim. */
228
+ rb_str_buf_append(buf, rb_String(name));
229
+ rb_str_buf_cat_ascii(buf, ":");
230
+ cursor += mb;
231
+ }
232
+
233
+ /* Trailing padding (DATA_SIZE > last member's end). Elided form
234
+ (see the alignment-gap branch above for rationale). */
235
+ if (cursor < data_size) {
236
+ char pad_decl[32];
237
+ snprintf(pad_decl, sizeof(pad_decl), "%lldx:",
238
+ (long long) (data_size - cursor));
239
+ rb_str_buf_cat_ascii(buf, pad_decl);
240
+ }
241
+
242
+ rb_str_buf_cat_ascii(buf, "}");
243
+ rb_obj_freeze(buf);
244
+ rb_ivar_set(data_class, iv, buf);
245
+ return buf;
246
+
247
+ unsupported:
248
+ /* Cache Qfalse to remember "we tried and gave up" — avoids
249
+ re-walking MEMBER_TABLE on every wrap call. */
250
+ rb_ivar_set(data_class, iv, Qfalse);
251
+ return Qnil;
252
+ }
253
+
254
+ static int
255
+ ca_mv_data_type_exportable (int8_t data_type)
256
+ {
257
+ return ca_mv_format_for(data_type) != NULL;
258
+ }
259
+
260
+ /* Same as ca_mv_data_type_exportable, but also accepts CA_FIXLEN:
261
+ - with a data_class -> emit PEP 3118 "T{...}" struct format
262
+ - without a data_class -> emit PEP 3118 "Ns" fixed-bytes format */
263
+ static int
264
+ ca_mv_ca_exportable (CArray *ca, VALUE obj)
265
+ {
266
+ if (ca_mv_data_type_exportable(ca->data_type)) return 1;
267
+ if (ca->data_type == CA_FIXLEN) {
268
+ if (RTEST(rb_ca_has_data_class(obj))) {
269
+ VALUE klass = rb_ca_data_class(obj);
270
+ if (NIL_P(klass)) return 0;
271
+ VALUE fmt = ca_mv_struct_format_for_data_class(klass);
272
+ return NIL_P(fmt) ? 0 : 1;
273
+ }
274
+ /* Plain CA_FIXLEN (bytes > 0) exports as "Ns". */
275
+ return (ca->bytes > 0) ? 1 : 0;
276
+ }
277
+ return 0;
278
+ }
279
+
280
+ /* Reverse mapping for the importer (Postel consumer; see
281
+ docs/interop/MemoryViewFormat.md for the contract):
282
+ - Strip a leading byte-order prefix ('<', '>', '=') if it matches host
283
+ endian; reject if it disagrees.
284
+ - Strip the alignment modifier '|'.
285
+ - Reject endian-bearing primary specifiers ('e','g','E','G','n','v',
286
+ 'N','V'); cross-endian sources are not accepted.
287
+ - Dispatch on the (stripped_format, item_size) tuple.
288
+ - Canonical: '?' for bool, 'Zf' / 'Zd' for complex64/128 (PEP 3118).
289
+ - Synonyms accepted but not emitted: 'C'/1 maps to UINT8 (so other
290
+ producers' bool buffers are received as UINT8); 'ff' / 'dd' map to
291
+ complex64/128 (producers that split complex into two floats). */
292
+ static int8_t
293
+ ca_mv_data_type_from_format (const char *format, ssize_t item_size)
294
+ {
295
+ const char *fmt = format ? format : "";
296
+ /* Strip alignment and host-matching byte-order prefix. */
297
+ if (*fmt == '|') fmt++;
298
+ if (*fmt == '<' || *fmt == '>' || *fmt == '=') {
299
+ /* The carray runtime stores data in host-endian; cross-endian
300
+ sources are rejected. */
301
+ int host_is_le = (ca_endian == CA_LITTLE_ENDIAN);
302
+ int wants_le = (*fmt == '<') || (*fmt == '=' && host_is_le);
303
+ int wants_be = (*fmt == '>') || (*fmt == '=' && !host_is_le);
304
+ if ((wants_le && !host_is_le) || (wants_be && host_is_le)) return -1;
305
+ fmt++;
306
+ }
307
+ if (*fmt == '\0') return -1;
308
+
309
+ /* single-byte formats */
310
+ if (strcmp(fmt, "c") == 0 && item_size == 1) return CA_INT8;
311
+ if (strcmp(fmt, "C") == 0 && item_size == 1) return CA_UINT8;
312
+ if (strcmp(fmt, "b") == 0 && item_size == 1) return CA_INT8;
313
+ if (strcmp(fmt, "B") == 0 && item_size == 1) return CA_UINT8;
314
+ if (strcmp(fmt, "?") == 0 && item_size == 1) return CA_BOOLEAN;
315
+
316
+ /* 16-bit */
317
+ if ((strcmp(fmt, "s") == 0 || strcmp(fmt, "s!") == 0 ||
318
+ strcmp(fmt, "h") == 0) && item_size == 2) return CA_INT16;
319
+ if ((strcmp(fmt, "S") == 0 || strcmp(fmt, "S!") == 0 ||
320
+ strcmp(fmt, "H") == 0) && item_size == 2) return CA_UINT16;
321
+
322
+ /* 32-bit */
323
+ if ((strcmp(fmt, "l") == 0 || strcmp(fmt, "i") == 0 ||
324
+ strcmp(fmt, "i!") == 0 || strcmp(fmt, "l!") == 0) && item_size == 4) return CA_INT32;
325
+ if ((strcmp(fmt, "L") == 0 || strcmp(fmt, "I") == 0 ||
326
+ strcmp(fmt, "I!") == 0 || strcmp(fmt, "L!") == 0) && item_size == 4) return CA_UINT32;
327
+
328
+ /* 64-bit. `l`/`L` at item_size 8 accepted as LP64 native long
329
+ (numpy default for `np.array([1,2,3]).dtype == int64`). */
330
+ if ((strcmp(fmt, "q") == 0 || strcmp(fmt, "q!") == 0 ||
331
+ strcmp(fmt, "l!") == 0 || strcmp(fmt, "l") == 0) && item_size == 8) return CA_INT64;
332
+ if ((strcmp(fmt, "Q") == 0 || strcmp(fmt, "Q!") == 0 ||
333
+ strcmp(fmt, "L!") == 0 || strcmp(fmt, "L") == 0) && item_size == 8) return CA_UINT64;
334
+
335
+ /* floats: no endian-bearing variants accepted */
336
+ if (strcmp(fmt, "f") == 0 && item_size == 4) return CA_FLOAT32;
337
+ if (strcmp(fmt, "d") == 0 && item_size == 8) return CA_FLOAT64;
338
+
339
+ /* complex: PEP 3118 'Zf' / 'Zd' canonical. */
340
+ if (strcmp(fmt, "Zf") == 0 && item_size == 8) return CA_CMPLX64;
341
+ if (strcmp(fmt, "Zd") == 0 && item_size == 16) return CA_CMPLX128;
342
+ /* Consumer synonyms — accepted but not emitted. */
343
+ if (strcmp(fmt, "ff") == 0 && item_size == 8) return CA_CMPLX64;
344
+ if (strcmp(fmt, "dd") == 0 && item_size == 16) return CA_CMPLX128;
345
+
346
+ /* Fixed-length bytes: PEP 3118 "Ns" (e.g. "8s") -> CA_FIXLEN, bytes = N.
347
+ N must match item_size exactly; mismatch is rejected the same way the
348
+ numeric branches reject a width disagreement. N must be positive
349
+ (rejects "0s" — CA_FIXLEN's bytes > 0 invariant). */
350
+ {
351
+ size_t len = strlen(fmt);
352
+ if (len >= 2 && fmt[len - 1] == 's') {
353
+ char *endp = NULL;
354
+ long n = strtol(fmt, &endp, 10);
355
+ if (endp == fmt + len - 1 && n > 0 && n == (long) item_size) {
356
+ return CA_FIXLEN;
357
+ }
358
+ }
359
+ }
360
+
361
+ return -1;
362
+ }
363
+
364
+ /* Derive the data_type a MemoryView producer would import as, by parsing its
365
+ format — without importing the data. This is the canonical "what dtype is
366
+ this MV" question, decoupled from the import strategy (copy via
367
+ from_memory_view vs zero-copy via wrap_memory_view). Used by
368
+ ca_arg_to_data_type so CArray.result_type / promote_list can treat an MV
369
+ producer as an operand. Returns -1 (so the caller falls back to value
370
+ inference) when obj is not an MV producer, is typeless (format == NULL), or
371
+ carries a format carray cannot represent host-endian. */
372
+ int8_t
373
+ ca_mv_probe_data_type (VALUE obj)
374
+ {
375
+ rb_memory_view_t view;
376
+ int8_t data_type;
377
+ if ( ! rb_memory_view_available_p(obj) ) {
378
+ return -1;
379
+ }
380
+ /* Zero-init before get: release() unconditionally frees item_desc.components
381
+ even on producers that leave it uninitialized (see ca_mv_acquire_and_validate). */
382
+ memset(&view, 0, sizeof(view));
383
+ if ( ! rb_memory_view_get(obj, &view, RUBY_MEMORY_VIEW_STRIDES) ) {
384
+ if ( ! rb_memory_view_get(obj, &view, RUBY_MEMORY_VIEW_SIMPLE) ) {
385
+ return -1;
386
+ }
387
+ }
388
+ data_type = ca_mv_data_type_from_format(view.format, view.item_size);
389
+ rb_memory_view_release(&view);
390
+ return data_type;
391
+ }
392
+
393
+ /* Per-element byte width for consumer-side wrap/from CArray allocation.
394
+ For numeric types it is `ca_sizeof[data_type]`; for CA_FIXLEN it is the
395
+ MemoryView item_size, since fixlen has no compile-time element width. */
396
+ static inline ca_size_t
397
+ ca_mv_carray_bytes_for (int8_t data_type, ssize_t item_size)
398
+ {
399
+ return (data_type == CA_FIXLEN) ? (ca_size_t) item_size
400
+ : (ca_size_t) ca_sizeof[data_type];
401
+ }
402
+
403
+ /* ---------------- strategy lookup ---------------- */
404
+
405
+ static ca_mv_strategy_t
406
+ ca_mv_strategy_for (int16_t obj_type)
407
+ {
408
+ /* compile-time enum members */
409
+ if (obj_type == CA_OBJ_ARRAY ||
410
+ obj_type == CA_OBJ_ARRAY_WRAP ||
411
+ obj_type == CA_OBJ_SCALAR) return CA_MV_DIRECT;
412
+ if (obj_type == CA_OBJ_REFER) return CA_MV_REFER;
413
+ if (obj_type == CA_OBJ_BLOCK) return CA_MV_BLOCK;
414
+ if (obj_type == CA_OBJ_SELECT) return CA_MV_ATTACH;
415
+ if (obj_type == CA_OBJ_REPEAT) return CA_MV_REPEAT;
416
+ if (obj_type == CA_OBJ_OBJECT) return CA_MV_REJECT;
417
+ if (obj_type == CA_OBJ_UNBOUND_REPEAT) return CA_MV_REJECT;
418
+ /* Plain CAStride (created via #as_strided, or wrap_memory_view from
419
+ a strided producer). Reuse the CA_MV_TRANS handler since it
420
+ already reads strides[] / base_offset directly from the CAStride
421
+ layout -- no CATranspose-specific code in that path anymore. */
422
+ if (obj_type == CA_OBJ_STRIDE) return CA_MV_TRANS;
423
+
424
+ /* runtime-assigned obj_types */
425
+ for (int i = 0; i < CA_MV_NUM_RUNTIME_OBJ_TYPES; i++) {
426
+ if (ca_mv_runtime_types[i].id == obj_type) {
427
+ return (ca_mv_strategy_t) ca_mv_runtime_types[i].strategy;
428
+ }
429
+ }
430
+ return CA_MV_REJECT;
431
+ }
432
+
433
+ /* ---------------- helpers ---------------- */
434
+
435
+ static int
436
+ ca_mv_extract (VALUE obj, CArray **out)
437
+ {
438
+ if (! rb_typeddata_is_kind_of(obj, &carray_data_type)) {
439
+ return 0;
440
+ }
441
+ *out = (CArray *) DATA_PTR(obj);
442
+ return 1;
443
+ }
444
+
445
+ static ssize_t *
446
+ ca_mv_alloc_shape (CArray *ca)
447
+ {
448
+ ssize_t *shape;
449
+ int i;
450
+ if (ca->ndim <= 0) return NULL;
451
+ shape = (ssize_t *) xmalloc(sizeof(ssize_t) * ca->ndim);
452
+ for (i = 0; i < ca->ndim; i++) {
453
+ shape[i] = (ssize_t) ca->dim[i];
454
+ }
455
+ return shape;
456
+ }
457
+
458
+ static ssize_t *
459
+ ca_mv_alloc_strides_contiguous (CArray *ca, ssize_t *shape, bool row_major)
460
+ {
461
+ ssize_t *strides;
462
+ if (ca->ndim <= 0) return NULL;
463
+ strides = (ssize_t *) xmalloc(sizeof(ssize_t) * ca->ndim);
464
+ rb_memory_view_fill_contiguous_strides(
465
+ (ssize_t) ca->ndim, (ssize_t) ca->bytes, shape, row_major, strides);
466
+ return strides;
467
+ }
468
+
469
+ /* Byte strides for the CAStride family come from
470
+ ca_stride_compose_to_root, which produces them directly from the
471
+ CAStride prefix (composing through any CAStride parent chain to
472
+ entity); no per-view stride helpers live here. */
473
+
474
+ /* ---------------- alias-chain wrap predicate ----------------
475
+ wrap_memory_view exports zero-copy. A view is wrappable iff its
476
+ parent chain bottoms out at an entity through links that all
477
+ alias (contig CAStride family). Other ancestors (CASelect,
478
+ CAFake, non-contig CAStride, etc.) would require materialising a
479
+ snapshot, which has incorrect view semantics for MV consumers
480
+ (deferred sync on release, stale reads, no explicit-sync API).
481
+ Consumers that want a snapshot should use CArray.from_memory_view. */
482
+
483
+ typedef struct {
484
+ int depth; /* 0 = the view itself, 1 = parent, 2 = grand-, ... */
485
+ const char *class_name; /* class name of the offending link */
486
+ const char *reason; /* short explanation */
487
+ } ca_mv_reject_t;
488
+
489
+ static bool ca_mv_stride_is_contig (CAStride *ca); /* defined below */
490
+
491
+ /* True iff `ca`'s obj_type uses ca_stride_func's attach routine -- i.e.
492
+ it is CAStride or a subclass that inherits CAStride semantics. */
493
+ static inline bool
494
+ ca_mv_is_castride_family (CArray *ca)
495
+ {
496
+ extern ca_operation_function_t ca_stride_func;
497
+ return ca_func[ca->obj_type].attach == ca_stride_func.attach;
498
+ }
499
+
500
+ static const char *
501
+ ca_mv_class_name_of (CArray *ca)
502
+ {
503
+ VALUE klass = ca_class[ca->obj_type];
504
+ return NIL_P(klass) ? "(unknown)" : rb_class2name(klass);
505
+ }
506
+
507
+ static const char *
508
+ ca_mv_reject_reason_for (CArray *ca)
509
+ {
510
+ /* Compile-time obj_types we know about */
511
+ if (ca->obj_type == CA_OBJ_SELECT) return "boolean-mask selection (positions not expressible as strides)";
512
+ if (ca->obj_type == CA_OBJ_OBJECT) return "stores Ruby VALUEs, not raw bytes";
513
+ if (ca->obj_type == CA_OBJ_UNBOUND_REPEAT) return "shape is not bound";
514
+ /* Runtime-assigned obj_types: look up by name in the strategy table */
515
+ for (int i = 0; i < CA_MV_NUM_RUNTIME_OBJ_TYPES; i++) {
516
+ if (ca_mv_runtime_types[i].id == ca->obj_type) {
517
+ const char *n = ca_mv_runtime_types[i].class_name;
518
+ if (!strcmp(n, "CAGrid")) return "grid selection (positions not expressible as strides)";
519
+ if (!strcmp(n, "CAShift")) return "shifted view with bounds-fill (no in-place inverse)";
520
+ if (!strcmp(n, "CAWindow")) return "windowed view with bounds-fill (no in-place inverse)";
521
+ if (!strcmp(n, "CAFake")) return "lazy type conversion (values differ from underlying bytes)";
522
+ /* CAField exports as a zero-copy CAStride and never hits this
523
+ diagnostic. */
524
+ if (!strcmp(n, "CAReduce")) return "OR-reduce (no in-place inverse)";
525
+ if (!strcmp(n, "CABitarray")) return "sub-byte (bit) addressing";
526
+ if (!strcmp(n, "CABitfield")) return "sub-byte (bitfield) addressing";
527
+ }
528
+ }
529
+ return "this kind of view is not alias-capable";
530
+ }
531
+
532
+ /* Resolve ca to an (entity, composed_strides, composed_base) triple
533
+ suitable for zero-copy strided export. Uses ca_stride_compose_to_root
534
+ to fold the CAStride parent chain (alias-eligible OR not, as long
535
+ as strides decompose cleanly into single parent-dim advances).
536
+
537
+ Succeeds when the resolved root is an entity (CArray/CAWrap/CScalar).
538
+ On failure (leaf is not CAStride family, or compose stops before
539
+ entity), fills *rej.
540
+
541
+ `out_strides` and `out_base` are filled only when ca is a CAStride
542
+ leaf (i.e. compose was performed). For entity-class ca, callers
543
+ compute contig row-major strides themselves. */
544
+ static bool
545
+ ca_mv_compose_to_entity (CArray *ca, ca_mv_reject_t *rej,
546
+ CArray **out_entity,
547
+ ca_size_t *out_strides,
548
+ ca_size_t *out_base)
549
+ {
550
+ /* Face (= CARecord etc.) is a 1:1 alias of parent->ptr, so
551
+ storage-layer-wise it is identical to the parent's entity / castride
552
+ layer. Strip Face, descend to parent, and feed the subsequent
553
+ compose. data_class / format come separately from the Face tail via
554
+ rb_ca_data_class universal dispatch. */
555
+ if (ca_is_face(ca)) {
556
+ ca = ca_strip_face(ca);
557
+ }
558
+ if (ca_is_entity(ca)) {
559
+ *out_entity = ca;
560
+ *out_base = 0;
561
+ return true;
562
+ }
563
+ if (!ca_mv_is_castride_family(ca)) {
564
+ rej->depth = 0;
565
+ rej->class_name = ca_mv_class_name_of(ca);
566
+ rej->reason = ca_mv_reject_reason_for(ca);
567
+ return false;
568
+ }
569
+ CArray *root;
570
+ ca_stride_compose_to_root((CAStride *) ca, &root, out_strides, out_base);
571
+ if (!ca_is_entity(root)) {
572
+ rej->depth = -1;
573
+ rej->class_name = ca_mv_class_name_of(root);
574
+ if (ca_mv_is_castride_family(root)) {
575
+ rej->reason = "CAStride chain stride pattern does not compose "
576
+ "linearly into entity (e.g. reshape across non-contig)";
577
+ } else {
578
+ rej->reason = ca_mv_reject_reason_for(root);
579
+ }
580
+ return false;
581
+ }
582
+ *out_entity = root;
583
+ return true;
584
+ }
585
+
586
+ /* Predicate wrapper for available_p / reject_reason: discards the
587
+ resolved (entity, strides, base). */
588
+ static bool
589
+ ca_mv_check_alias_chain (CArray *ca, ca_mv_reject_t *rej)
590
+ {
591
+ CArray *entity;
592
+ ca_size_t strides[CA_RANK_MAX];
593
+ ca_size_t base;
594
+ return ca_mv_compose_to_entity(ca, rej, &entity, strides, &base);
595
+ }
596
+
597
+ /* Local copy of ca_obj_stride.c's contiguity check (that one is
598
+ static). Returns true iff strides match a row-major contig
599
+ layout over the view's own dim. */
600
+ static bool
601
+ ca_mv_stride_is_contig (CAStride *ca)
602
+ {
603
+ ca_size_t expected = ca->bytes;
604
+ int8_t k;
605
+ for (k = ca->ndim - 1; k >= 0; k--) {
606
+ if (ca->dim[k] != 1 && ca->strides[k] != expected) return false;
607
+ expected *= ca->dim[k];
608
+ }
609
+ return true;
610
+ }
611
+
612
+ /* ---------------- get / release / available_p ---------------- */
613
+
614
+ static bool
615
+ ca_mv_available_p (VALUE obj)
616
+ {
617
+ CArray *ca;
618
+ ca_mv_reject_t rej;
619
+ if (! ca_mv_extract(obj, &ca)) return false;
620
+ if (! ca_mv_ca_exportable(ca, obj)) return false;
621
+ if (ca_has_mask(ca)) return false;
622
+ /* An entity whose obj_type has no strategy of its own is one installed
623
+ from outside the core (ca_install_obj_type). It reaches this callback
624
+ only because it inherits CArray's MemoryView registration, and
625
+ ca_mv_get would then reject it — say so here rather than advertising
626
+ an export that cannot be produced. Built-in entities (CArray /
627
+ CAWrap / CScalar) map to DIRECT and are unaffected. */
628
+ if (ca_is_entity(ca) && ca_mv_strategy_for(ca->obj_type) == CA_MV_REJECT) {
629
+ return false;
630
+ }
631
+ /* Wrap-via-MV exports are zero-copy: require an alias chain to
632
+ entity. Non-alias views (CASelect, CAFake, etc.) and CAStride
633
+ chains broken by a non-contig link cannot be wrapped without a
634
+ snapshot, which has wrong semantics for MV consumers. */
635
+ return ca_mv_check_alias_chain(ca, &rej);
636
+ }
637
+
638
+ static bool
639
+ ca_mv_get (VALUE obj, rb_memory_view_t *view, int flags)
640
+ {
641
+ CArray *ca;
642
+ ca_mv_strategy_t strategy;
643
+ ssize_t *shape = NULL;
644
+ ssize_t *strides = NULL;
645
+ void *data_ptr = NULL;
646
+ int writable_request;
647
+
648
+ if (! ca_mv_extract(obj, &ca)) return false;
649
+ if (! ca_mv_ca_exportable(ca, obj)) return false;
650
+ if (ca_has_mask(ca)) return false;
651
+
652
+ writable_request = (flags & RUBY_MEMORY_VIEW_WRITABLE) ? 1 : 0;
653
+ if (writable_request && ca_is_readonly(ca)) return false;
654
+ /* Defense in depth: reject WRITABLE requests on Ruby-level frozen objects
655
+ even if CA_FLAG_READ_ONLY wasn't set (e.g., freeze bypassed via
656
+ Object#freeze on a subclass). */
657
+ if (writable_request && OBJ_FROZEN(obj)) return false;
658
+
659
+ /* Resolve the chain to (entity, composed_strides, composed_base).
660
+ If ca isn't a CAStride family member, only entity-class is
661
+ accepted here. The CAStride-family case below uses composed_strides
662
+ / composed_base directly; the DIRECT case ignores them (entity owns
663
+ ca->ptr already, no compose needed). */
664
+ CArray *resolved_entity = NULL;
665
+ ca_size_t composed_strides[CA_RANK_MAX];
666
+ ca_size_t composed_base = 0;
667
+ {
668
+ ca_mv_reject_t rej;
669
+ if (! ca_mv_compose_to_entity(ca, &rej, &resolved_entity,
670
+ composed_strides, &composed_base)) {
671
+ return false;
672
+ }
673
+ }
674
+
675
+ /* SIMPLE consumer on non-contig view would need a snapshot -- not
676
+ offered (consumers wanting a snapshot use from_memory_view). */
677
+ {
678
+ int wants_strides_early = (flags & RUBY_MEMORY_VIEW_STRIDES) ? 1 : 0;
679
+ if (!wants_strides_early &&
680
+ ca_mv_is_castride_family(ca) &&
681
+ !ca_mv_stride_is_contig((CAStride *) ca)) {
682
+ return false;
683
+ }
684
+ }
685
+
686
+ strategy = ca_mv_strategy_for(ca->obj_type);
687
+
688
+ switch (strategy) {
689
+ case CA_MV_DIRECT:
690
+ /* Entity / wrap / scalar: ca->ptr is the entity's own buffer, always
691
+ valid. No attach needed. */
692
+ data_ptr = ca->ptr;
693
+ if (ca->ndim >= 1) {
694
+ shape = ca_mv_alloc_shape(ca);
695
+ strides = ca_mv_alloc_strides_contiguous(ca, shape, true);
696
+ }
697
+ break;
698
+
699
+ /* All CAStride-family views (CARefer / CABlock / CAFarray / CATranspose
700
+ / CARepeat / plain CAStride) reach this point only after
701
+ ca_mv_compose_to_entity succeeded -- the producer is alias-only by
702
+ ca_mv_available_p contract. Expose `entity->ptr + composed_base` as
703
+ data_ptr and composed_strides as strides, both in entity byte space.
704
+ The entity owns its bytes (always valid via view->obj's parent chain
705
+ while the consumer holds the view).
706
+
707
+ CAREFUL: do not ca_attach the view here (nor ca_sync / ca_detach at
708
+ release). Exposure is alias-only: the consumer writes directly into
709
+ root bytes, so a sync would scatter bytes back to root as a no-op
710
+ chain -- and that chain triggers ca_update_mask / ca_sync(mask)
711
+ recursion that can raise on an invariant violation. Raising from a
712
+ release callback is fatal during GC sweep (newobj_of during sweep ->
713
+ BUG). */
714
+ case CA_MV_REFER:
715
+ case CA_MV_BLOCK:
716
+ case CA_MV_FARRAY:
717
+ case CA_MV_TRANS:
718
+ case CA_MV_REPEAT: {
719
+ int8_t k;
720
+ shape = ca_mv_alloc_shape(ca);
721
+ strides = (ssize_t *) xmalloc(sizeof(ssize_t) * ca->ndim);
722
+ for (k = 0; k < ca->ndim; k++) strides[k] = (ssize_t) composed_strides[k];
723
+ data_ptr = resolved_entity->ptr + composed_base;
724
+ break;
725
+ }
726
+
727
+ case CA_MV_ATTACH:
728
+ /* Should be unreachable: ca_mv_check_alias_chain rejects every
729
+ obj_type that maps to ATTACH (CASelect / CAMapping / CAGrid /
730
+ CAShift / CAWindow / CAFake / CAReduce). Materialise is no
731
+ longer offered via wrap_memory_view; from_memory_view gives a
732
+ snapshot copy instead. */
733
+ rb_raise(rb_eRuntimeError,
734
+ "[BUG] ATTACH strategy reached in ca_mv_get for %s; "
735
+ "alias-chain check should have rejected it",
736
+ ca_mv_class_name_of(ca));
737
+
738
+ case CA_MV_REJECT:
739
+ default:
740
+ return false;
741
+ }
742
+
743
+ view->obj = obj;
744
+ view->data = data_ptr;
745
+ /* PEP 3118 convention: byte_size is product(shape) * item_size for
746
+ every layout (contiguous or strided). NumPy, Numo, and Arrow all
747
+ follow this; consumers that need the addressable span for strided
748
+ views compute Σ (shape[k]-1)*|strides[k]| + item_size themselves. */
749
+ view->byte_size = (ssize_t) ca_length(ca);
750
+ view->readonly = ca_is_readonly(ca) ? true : false;
751
+ /* Format string: primitive types use the top-level PEP 3118 table;
752
+ CA_FIXLEN + data_class emits a PEP 3118 "T{...}" struct format
753
+ whose lifetime is bound to the class via @__mv_struct_format__,
754
+ so the view->format pointer is stable until the class is GC'd. */
755
+ if (ca->data_type == CA_FIXLEN && RTEST(rb_ca_has_data_class(obj))) {
756
+ VALUE klass = rb_ca_data_class(obj);
757
+ VALUE fmt = ca_mv_struct_format_for_data_class(klass);
758
+ view->format = RSTRING_PTR(fmt);
759
+ }
760
+ else if (ca->data_type == CA_FIXLEN) {
761
+ /* Plain CA_FIXLEN (no data_class): emit PEP 3118 "Ns" fixed-bytes.
762
+ Cache the format String on the source object so RSTRING_PTR stays
763
+ stable for the view's lifetime; the source object is anchored as
764
+ view->obj. */
765
+ VALUE fmt_str = rb_attr_get(obj, rb_intern("__mv_fixlen_format__"));
766
+ if (NIL_P(fmt_str)) {
767
+ char fmt_buf[32];
768
+ snprintf(fmt_buf, sizeof(fmt_buf), "%lds", (long) ca->bytes);
769
+ fmt_str = rb_str_new_cstr(fmt_buf);
770
+ rb_obj_freeze(fmt_str);
771
+ rb_ivar_set(obj, rb_intern("__mv_fixlen_format__"), fmt_str);
772
+ }
773
+ view->format = RSTRING_PTR(fmt_str);
774
+ }
775
+ else {
776
+ view->format = ca_mv_format_for(ca->data_type);
777
+ }
778
+ view->item_size = (ssize_t) ca->bytes;
779
+ view->item_desc.components = NULL;
780
+ view->item_desc.length = 0;
781
+ view->ndim = (ca->obj_type == CA_OBJ_SCALAR) ? 0 : (ssize_t) ca->ndim;
782
+ view->shape = (view->ndim == 0) ? NULL : shape;
783
+ view->strides = (view->ndim == 0) ? NULL : strides;
784
+ view->sub_offsets = NULL;
785
+ view->private_data = NULL;
786
+
787
+ /* Enforce caller's layout request. */
788
+ {
789
+ int wants_strides = (flags & RUBY_MEMORY_VIEW_STRIDES) ? 1 : 0;
790
+ int wants_row = (flags & RUBY_MEMORY_VIEW_ROW_MAJOR) == RUBY_MEMORY_VIEW_ROW_MAJOR;
791
+ int wants_col = (flags & RUBY_MEMORY_VIEW_COLUMN_MAJOR) == RUBY_MEMORY_VIEW_COLUMN_MAJOR;
792
+ bool row_ok = (view->ndim == 0) ? true : rb_memory_view_is_row_major_contiguous(view);
793
+ bool col_ok = (view->ndim == 0) ? true : rb_memory_view_is_column_major_contiguous(view);
794
+ if (wants_row && !row_ok) goto reject;
795
+ if (wants_col && !col_ok) goto reject;
796
+ if (!wants_strides && !row_ok) goto reject;
797
+ }
798
+
799
+ return true;
800
+
801
+ reject:
802
+ /* Layout-mismatch reject: caller wanted SIMPLE/contig but we built a
803
+ strided view, or vice versa. ca_mv_get is alias-only and does not
804
+ attach anything, so just free the helper shape/strides buffers and
805
+ return false; there is no attach state to unwind. */
806
+ if (shape) { xfree(shape); view->shape = NULL; }
807
+ if (strides) { xfree(strides); view->strides = NULL; }
808
+ return false;
809
+ }
810
+
811
+ static bool
812
+ ca_mv_release (VALUE obj, rb_memory_view_t *view)
813
+ {
814
+ (void) obj;
815
+ if (view->shape) {
816
+ xfree((void *) view->shape);
817
+ view->shape = NULL;
818
+ }
819
+ if (view->strides) {
820
+ xfree((void *) view->strides);
821
+ view->strides = NULL;
822
+ }
823
+ /* CAREFUL: no attach/sync/detach here. ca_mv_get is alias-only -- the
824
+ consumer wrote directly into root bytes, so a ca_sync would scatter
825
+ back as a no-op chain that can trigger ca_update_mask /
826
+ ca_sync(ca->mask) recursion and raise; raising from a release
827
+ callback is fatal during GC sweep. See ca_mv_get's CAStride-family
828
+ case comment for the full rationale. */
829
+ view->private_data = NULL;
830
+ return true;
831
+ }
832
+
833
+ /* ---------------- importer ----------------
834
+ Two-tier API mirroring numo-narray-memoryview:
835
+
836
+ CArray.from_memory_view(obj) -> CArray (copy, independent buffer)
837
+ CArray.wrap_memory_view(obj) -> CAWrap (zero-copy, shared memory)
838
+
839
+ from_memory_view: borrow a SIMPLE view, copy bytes into a freshly
840
+ owned CArray, release the view immediately. The result is detached
841
+ from the source.
842
+
843
+ wrap_memory_view: borrow a SIMPLE view and keep it alive for the
844
+ lifetime of a CAWrap. The source object stays anchored via an ivar;
845
+ a TypedData holder (also stored as an ivar) releases the view in its
846
+ dfree callback when the CAWrap is collected. */
847
+
848
+ typedef struct {
849
+ rb_memory_view_t view;
850
+ bool valid;
851
+ } ca_mv_imported_holder_t;
852
+
853
+ static void
854
+ ca_mv_imported_holder_free (void *p)
855
+ {
856
+ ca_mv_imported_holder_t *h = (ca_mv_imported_holder_t *) p;
857
+ if (h->valid) {
858
+ rb_memory_view_release(&h->view);
859
+ h->valid = false;
860
+ }
861
+ xfree(h);
862
+ }
863
+
864
+ static size_t
865
+ ca_mv_imported_holder_size (const void *p)
866
+ {
867
+ return sizeof(ca_mv_imported_holder_t);
868
+ }
869
+
870
+ static const rb_data_type_t ca_mv_imported_holder_data_type = {
871
+ .wrap_struct_name = "CArrayImportedMemoryViewHolder",
872
+ .function = {
873
+ .dmark = NULL, /* source_obj is anchored via ivar on the wrapper */
874
+ .dfree = ca_mv_imported_holder_free,
875
+ .dsize = ca_mv_imported_holder_size,
876
+ .dcompact = NULL
877
+ },
878
+ .flags = RUBY_TYPED_FREE_IMMEDIATELY
879
+ };
880
+
881
+ /* Acquire a MemoryView from src and validate/derive data_type, ndim
882
+ and shape. Tries STRIDES first; falls back to SIMPLE so that
883
+ producers that advertise only contiguous still work. On any
884
+ failure, releases the view (if acquired) and raises ArgumentError;
885
+ on success, the view is left active and the caller owns the
886
+ responsibility to release it.
887
+
888
+ `explicit_data_type` is the caller-supplied target data_type (>= 0) or -1
889
+ if the caller has no preference. Behaviour:
890
+ - explicit_data_type < 0, format != NULL: derive data_type from format.
891
+ - explicit_data_type < 0, format == NULL: reject (typeless source
892
+ without consumer-supplied type cannot be interpreted).
893
+ - explicit_data_type >= 0, format != NULL: must match the format's
894
+ data_type; mismatch -> reject.
895
+ - explicit_data_type >= 0, format == NULL: use explicit_data_type; the
896
+ byte buffer is reinterpreted as that data_type. ndim is forced
897
+ to 1 (multi-dim from a typeless source is ambiguous; user
898
+ chains .reshape).
899
+ */
900
+ /* Row-major contiguity test that tolerates a NULL shape array. ndim-0
901
+ scalars and SIMPLE-form producers (which omit the shape array for a
902
+ contiguous 1-D buffer, e.g. red-arrow's primitive arrays) are
903
+ contiguous by construction; rb_memory_view_is_row_major_contiguous
904
+ dereferences view->shape, so it must not be reached with shape NULL. */
905
+ static bool
906
+ ca_mv_view_is_contiguous (const rb_memory_view_t *view)
907
+ {
908
+ if (view->ndim == 0 || view->shape == NULL) {
909
+ return true;
910
+ }
911
+ return rb_memory_view_is_row_major_contiguous(view);
912
+ }
913
+
914
+ static void
915
+ ca_mv_acquire_and_validate (VALUE src, rb_memory_view_t *view,
916
+ int8_t explicit_data_type,
917
+ int8_t *out_data_type, int8_t *out_ndim,
918
+ ca_size_t *out_dim)
919
+ {
920
+ int8_t data_type;
921
+ int i;
922
+
923
+ /* Defensive zero-init: rb_memory_view_release() unconditionally
924
+ xfree()s view->item_desc.components, but not every producer writes
925
+ that field. red-arrow 24.0.0 leaves it uninitialized, so a view
926
+ acquired from it frees stack garbage on release (abort). Zeroing
927
+ here makes the field NULL. Consumer side of apache/arrow#45187
928
+ (fixed in Arrow 25.0.0); harmless once producers initialize it. */
929
+ memset(view, 0, sizeof(*view));
930
+
931
+ if (! rb_memory_view_get(src, view, RUBY_MEMORY_VIEW_STRIDES)) {
932
+ if (! rb_memory_view_get(src, view, RUBY_MEMORY_VIEW_SIMPLE)) {
933
+ rb_raise(rb_eArgError,
934
+ "object does not support MemoryView");
935
+ }
936
+ }
937
+
938
+ if (view->format == NULL) {
939
+ /* Typeless producer (e.g. mmap-view, IO#read-style byte blob).
940
+ Consumer must supply the target data_type. */
941
+ if (explicit_data_type < 0) {
942
+ ssize_t got = view->byte_size;
943
+ rb_memory_view_release(view);
944
+ rb_raise(rb_eArgError,
945
+ "typeless MemoryView (format=NULL, %ld bytes); "
946
+ "specify the target data_type via data_type: kwarg or use a "
947
+ "concrete CArray::<Dtype> factory class",
948
+ (long) got);
949
+ }
950
+ if (ca_mv_format_for(explicit_data_type) == NULL) {
951
+ rb_memory_view_release(view);
952
+ rb_raise(rb_eArgError,
953
+ "target data_type is not representable via MemoryView");
954
+ }
955
+ ssize_t target_item_size = (ssize_t) ca_sizeof[explicit_data_type];
956
+ if (view->byte_size % target_item_size != 0) {
957
+ ssize_t bs = view->byte_size;
958
+ rb_memory_view_release(view);
959
+ rb_raise(rb_eArgError,
960
+ "byte_size %ld is not a multiple of target item_size %ld",
961
+ (long) bs, (long) target_item_size);
962
+ }
963
+ data_type = explicit_data_type;
964
+ *out_ndim = 1;
965
+ out_dim[0] = (ca_size_t) (view->byte_size / target_item_size);
966
+ *out_data_type = data_type;
967
+ return;
968
+ }
969
+
970
+ /* Typed producer: derive data_type from format. */
971
+ data_type = ca_mv_data_type_from_format(view->format, view->item_size);
972
+ if (data_type < 0) {
973
+ char fmt_buf[64];
974
+ snprintf(fmt_buf, sizeof(fmt_buf), "%s", view->format);
975
+ rb_memory_view_release(view);
976
+ rb_raise(rb_eArgError,
977
+ "unsupported MemoryView format: %s (item_size=%ld)",
978
+ fmt_buf, (long) view->item_size);
979
+ }
980
+
981
+ /* If the caller specified a data_type too, it must match. */
982
+ if (explicit_data_type >= 0 && explicit_data_type != data_type) {
983
+ rb_memory_view_release(view);
984
+ rb_raise(rb_eArgError,
985
+ "explicit data_type does not match producer format "
986
+ "(producer: %s, requested data_type %d)",
987
+ view->format, (int) explicit_data_type);
988
+ }
989
+
990
+ if (view->ndim == 0) {
991
+ *out_ndim = 1;
992
+ out_dim[0] = 1;
993
+ }
994
+ else if (view->ndim > CA_RANK_MAX) {
995
+ long got = (long) view->ndim;
996
+ rb_memory_view_release(view);
997
+ rb_raise(rb_eArgError,
998
+ "MemoryView ndim (%ld) exceeds CArray CA_RANK_MAX (%d)",
999
+ got, CA_RANK_MAX);
1000
+ }
1001
+ else if (view->shape == NULL) {
1002
+ /* SIMPLE form: a typed producer may advertise a format but omit the
1003
+ shape array for a contiguous 1-D buffer (red-arrow's primitive
1004
+ arrays do this). Only 1-D is interpretable without a shape --
1005
+ length = byte_size / item_size. A missing shape at ndim > 1 is
1006
+ unrecoverable. */
1007
+ if (view->ndim > 1) {
1008
+ long got = (long) view->ndim;
1009
+ rb_memory_view_release(view);
1010
+ rb_raise(rb_eArgError,
1011
+ "MemoryView reports ndim %ld but provides no shape array",
1012
+ got);
1013
+ }
1014
+ *out_ndim = 1;
1015
+ out_dim[0] = (ca_size_t) (view->item_size ? view->byte_size / view->item_size : 0);
1016
+ }
1017
+ else {
1018
+ *out_ndim = (int8_t) view->ndim;
1019
+ for (i = 0; i < *out_ndim; i++) {
1020
+ out_dim[i] = (ca_size_t) view->shape[i];
1021
+ }
1022
+ }
1023
+ *out_data_type = data_type;
1024
+ }
1025
+
1026
+ /* Extract the optional data_type: and mask: keywords from argv.
1027
+ Returns the resolved data_type (-1 if not specified) and fills
1028
+ *out_obj / *out_mask. data_type: accepts symbols (:int32), integer
1029
+ constants (CA_INT32), and data_type marker classes (CArray::Int32
1030
+ etc.); marker classes are resolved via their `DataType` constant (set
1031
+ up by lib/carray/construct.rb), since rb_ca_guess_type does not
1032
+ natively recognise them. */
1033
+ static int8_t
1034
+ ca_mv_extract_type_and_mask_kwargs (int argc, VALUE *argv,
1035
+ VALUE *out_obj, VALUE *out_mask)
1036
+ {
1037
+ volatile VALUE robj = Qnil, ropt = Qnil, rtype = Qnil, rmask = Qnil;
1038
+ int8_t data_type = -1;
1039
+ ca_size_t bytes;
1040
+ rb_scan_args(argc, argv, "1:", (VALUE *) &robj, (VALUE *) &ropt);
1041
+ if (! NIL_P(ropt)) {
1042
+ rb_scan_options(ropt, "data_type,mask", &rtype, &rmask);
1043
+ }
1044
+ *out_obj = robj;
1045
+ *out_mask = rmask;
1046
+ if (! NIL_P(rtype)) {
1047
+ if (RB_TYPE_P(rtype, T_CLASS) &&
1048
+ rb_const_defined(rtype, rb_intern("DataType"))) {
1049
+ rtype = rb_const_get(rtype, rb_intern("DataType"));
1050
+ }
1051
+ rb_ca_guess_type_and_bytes(rtype, INT2NUM(0), &data_type, &bytes);
1052
+ }
1053
+ return data_type;
1054
+ }
1055
+
1056
+ /* Acquire and validate a mask MemoryView against the data side's shape.
1057
+ Returns an allocated holder (caller owns; release via the imported
1058
+ holder TypedData dfree path) with view active on success.
1059
+
1060
+ On validation failure: releases the mask view, frees the mask holder,
1061
+ AND ALSO releases the supplied `data_view_to_release_on_fail` + frees
1062
+ the supplied `data_holder_to_free_on_fail` before raising. This
1063
+ avoids leaking the caller's already-acquired data side when mask
1064
+ validation aborts. Pass NULL for the data cleanup args if not
1065
+ applicable (e.g. mask validated for a stack-allocated data view in
1066
+ the copy path). */
1067
+ static ca_mv_imported_holder_t *
1068
+ ca_mv_acquire_mask_view (VALUE mask_src, int8_t data_ndim,
1069
+ const ca_size_t *data_dim,
1070
+ ca_mv_imported_holder_t *data_holder_to_free_on_fail,
1071
+ rb_memory_view_t *data_view_to_release_on_fail)
1072
+ {
1073
+ ca_mv_imported_holder_t *holder;
1074
+ rb_memory_view_t *mv;
1075
+ int i;
1076
+
1077
+ # define MV_MASK_CLEANUP_AND_RAISE(msg, ...) do { \
1078
+ if (holder) { \
1079
+ if (holder->valid) { rb_memory_view_release(&holder->view); } \
1080
+ xfree(holder); \
1081
+ } \
1082
+ if (data_view_to_release_on_fail) { \
1083
+ rb_memory_view_release(data_view_to_release_on_fail); \
1084
+ } \
1085
+ if (data_holder_to_free_on_fail) { \
1086
+ data_holder_to_free_on_fail->valid = false; \
1087
+ xfree(data_holder_to_free_on_fail); \
1088
+ } \
1089
+ rb_raise(rb_eArgError, msg, ##__VA_ARGS__); \
1090
+ } while (0)
1091
+
1092
+ holder = ALLOC(ca_mv_imported_holder_t);
1093
+ holder->valid = false;
1094
+
1095
+ /* Zero-init before get: same apache/arrow#45187 defense as the data
1096
+ path (release frees view->item_desc.components unconditionally). */
1097
+ memset(&holder->view, 0, sizeof(holder->view));
1098
+
1099
+ if (! rb_memory_view_get(mask_src, &holder->view, RUBY_MEMORY_VIEW_STRIDES)) {
1100
+ if (! rb_memory_view_get(mask_src, &holder->view, RUBY_MEMORY_VIEW_SIMPLE)) {
1101
+ MV_MASK_CLEANUP_AND_RAISE("mask: object does not support MemoryView");
1102
+ }
1103
+ }
1104
+ holder->valid = true;
1105
+ mv = &holder->view;
1106
+
1107
+ if (mv->format != NULL) {
1108
+ char c = mv->format[0];
1109
+ /* skip a leading byte-order/native prefix '<>=!@' */
1110
+ if (c == '<' || c == '>' || c == '=' || c == '!' || c == '@') {
1111
+ c = mv->format[1];
1112
+ }
1113
+ if (c != '?' && c != 'B' && c != 'b') {
1114
+ char fmt_buf[64];
1115
+ snprintf(fmt_buf, sizeof(fmt_buf), "%s", mv->format);
1116
+ MV_MASK_CLEANUP_AND_RAISE("mask: format must be bool/uint8/int8 (got %s)",
1117
+ fmt_buf);
1118
+ }
1119
+ }
1120
+ if (mv->item_size != 1) {
1121
+ ssize_t got = mv->item_size;
1122
+ MV_MASK_CLEANUP_AND_RAISE("mask: item_size must be 1 (got %ld)",
1123
+ (long) got);
1124
+ }
1125
+ if (mv->ndim != data_ndim) {
1126
+ int got = (int) mv->ndim;
1127
+ MV_MASK_CLEANUP_AND_RAISE("mask: ndim (%d) does not match data ndim (%d)",
1128
+ got, (int) data_ndim);
1129
+ }
1130
+ if (mv->shape == NULL) {
1131
+ /* SIMPLE-form mask (contiguous 1-D, shape omitted). item_size is 1
1132
+ (checked above), so byte_size is the element count. */
1133
+ ca_size_t mlen = (ca_size_t) mv->byte_size;
1134
+ if (data_ndim != 1 || mlen != data_dim[0]) {
1135
+ long ms = (long) mlen, ds = (long) (data_ndim == 1 ? data_dim[0] : -1);
1136
+ MV_MASK_CLEANUP_AND_RAISE(
1137
+ "mask: length (%ld) does not match data shape (%ld)", ms, ds);
1138
+ }
1139
+ }
1140
+ else {
1141
+ for (i = 0; i < data_ndim; i++) {
1142
+ if ((ca_size_t) mv->shape[i] != data_dim[i]) {
1143
+ long ms = (long) mv->shape[i], ds = (long) data_dim[i];
1144
+ MV_MASK_CLEANUP_AND_RAISE(
1145
+ "mask: shape[%d] (%ld) does not match data shape (%ld)",
1146
+ i, ms, ds);
1147
+ }
1148
+ }
1149
+ }
1150
+ if (! ca_mv_view_is_contiguous(mv)) {
1151
+ MV_MASK_CLEANUP_AND_RAISE(
1152
+ "mask: strided masks are not supported in this release; "
1153
+ "pass a row-major contiguous mask buffer");
1154
+ }
1155
+
1156
+ # undef MV_MASK_CLEANUP_AND_RAISE
1157
+ return holder;
1158
+ }
1159
+
1160
+ /* Copy a strided MemoryView into a contiguous row-major destination buffer.
1161
+ Walks indices in row-major order and reads from view->data using
1162
+ view->strides at each step. Assumes view->ndim >= 1, view->strides
1163
+ non-NULL, dst sized to hold elements * item_size. */
1164
+ static void
1165
+ ca_mv_copy_strided_to_contiguous (char *dst, const rb_memory_view_t *view,
1166
+ ca_size_t elements)
1167
+ {
1168
+ int8_t ndim = (int8_t) view->ndim;
1169
+ ssize_t item_size = view->item_size;
1170
+ ca_size_t idx[CA_RANK_MAX];
1171
+ ca_size_t n;
1172
+ int8_t k;
1173
+
1174
+ for (k = 0; k < ndim; k++) idx[k] = 0;
1175
+
1176
+ for (n = 0; n < elements; n++) {
1177
+ ssize_t src_off = 0;
1178
+ for (k = 0; k < ndim; k++) {
1179
+ src_off += (ssize_t) idx[k] * view->strides[k];
1180
+ }
1181
+ memcpy(dst, (const char *) view->data + src_off, (size_t) item_size);
1182
+ dst += item_size;
1183
+ /* increment row-major: last dim varies fastest */
1184
+ for (k = ndim - 1; k >= 0; k--) {
1185
+ idx[k]++;
1186
+ if (idx[k] < (ca_size_t) view->shape[k]) break;
1187
+ idx[k] = 0;
1188
+ }
1189
+ }
1190
+ }
1191
+
1192
+ /* CArray.wrap_memory_view(obj, data_type: nil) -- zero-copy CAWrap.
1193
+ Rejects non-row-major-contiguous sources with a helpful pointer to
1194
+ from_memory_view; the contiguous-only contract keeps the boundary
1195
+ between Numo/CArray internal invariants clean (see the design note
1196
+ on strided wrap rejection).
1197
+
1198
+ When the producer is typeless (format=NULL), `data_type:` must be
1199
+ supplied; the byte buffer is reinterpreted as that data_type, ndim=1.
1200
+
1201
+ The receiver picks the class of the result. Called on CArray itself
1202
+ it builds a CAWrap, as it always has; called on a subclass of CAWrap
1203
+ it builds that subclass, so a gem bridging a foreign buffer can name
1204
+ where the array came from without writing a C extension of its own.
1205
+ The class marks the provenance of this object only -- a view derived
1206
+ from it is a CABlock or a CAStride like any other. */
1207
+ static VALUE
1208
+ rb_ca_s_wrap_memory_view (int argc, VALUE *argv, VALUE klass)
1209
+ {
1210
+ ca_mv_imported_holder_t *holder;
1211
+ ca_mv_imported_holder_t *mask_holder = NULL;
1212
+ rb_memory_view_t *v;
1213
+ int8_t data_type, ndim;
1214
+ ca_size_t dim[CA_RANK_MAX];
1215
+ VALUE wrap, holder_obj, obj, mask_obj, target_class;
1216
+ int8_t explicit_data_type =
1217
+ ca_mv_extract_type_and_mask_kwargs(argc, argv,
1218
+ (VALUE *) &obj, (VALUE *) &mask_obj);
1219
+
1220
+ /* Resolve the class before anything is acquired, so this rejection
1221
+ needs no cleanup. CArray means CAWrap; any other receiver has to
1222
+ be a CAWrap the caller can actually be handed. */
1223
+ target_class = ( klass == rb_cCArray ) ? rb_cCAWrap : klass;
1224
+ if ( ! RTEST(rb_class_inherited_p(target_class, rb_cCAWrap)) ) {
1225
+ rb_raise(rb_eTypeError,
1226
+ "wrap_memory_view builds a CAWrap; %s is not a subclass of it",
1227
+ rb_class2name(target_class));
1228
+ }
1229
+
1230
+ holder = ALLOC(ca_mv_imported_holder_t);
1231
+ holder->valid = false;
1232
+
1233
+ ca_mv_acquire_and_validate(obj, &holder->view, explicit_data_type,
1234
+ &data_type, &ndim, dim);
1235
+ holder->valid = true;
1236
+ v = &holder->view;
1237
+
1238
+ /* Whether the producer is strided is only knowable once the view is
1239
+ acquired, and a strided one is returned as a CAStride wrapping an
1240
+ inner CAWrap -- so a caller who asked for a class would get back
1241
+ something that is not it. Refuse instead, and release first: this
1242
+ runs before the mask is acquired, so the holder is all there is to
1243
+ free. */
1244
+ if ( target_class != rb_cCAWrap &&
1245
+ v->ndim >= 1 && v->format != NULL && ! ca_mv_view_is_contiguous(v) ) {
1246
+ rb_memory_view_release(v);
1247
+ holder->valid = false;
1248
+ xfree(holder);
1249
+ rb_raise(rb_eArgError,
1250
+ "%s was requested, but this producer is strided, so the result "
1251
+ "would be a CAStride; pass a row-major contiguous producer, or "
1252
+ "use CArray.from_memory_view for a copy",
1253
+ rb_class2name(target_class));
1254
+ }
1255
+
1256
+ /* mask: paired buffer. Acquire + validate now (before building the
1257
+ wrap). On failure the helper releases both the mask view AND the
1258
+ already-acquired data view + holder, then raises. */
1259
+ if (! NIL_P(mask_obj)) {
1260
+ mask_holder = ca_mv_acquire_mask_view(mask_obj, ndim, dim,
1261
+ holder, &holder->view);
1262
+ /* Strided data side combined with mask: is not supported in this
1263
+ release. Catch this AFTER mask acquisition so the cleanup path can release
1264
+ both views uniformly. */
1265
+ if (v->ndim >= 1 && v->format != NULL &&
1266
+ ! ca_mv_view_is_contiguous(v)) {
1267
+ rb_memory_view_release(&mask_holder->view);
1268
+ mask_holder->valid = false;
1269
+ xfree(mask_holder);
1270
+ rb_memory_view_release(v);
1271
+ holder->valid = false;
1272
+ xfree(holder);
1273
+ rb_raise(rb_eArgError,
1274
+ "mask: combining mask: with a strided data source is not "
1275
+ "supported in this release; pass a row-major contiguous "
1276
+ "data source");
1277
+ }
1278
+ }
1279
+
1280
+ /* Strided producer: build a CAStride on top of an inner CAWrap that
1281
+ anchors the foreign memory. The inner CAWrap holds the MemoryView
1282
+ lifecycle (memory_view_source + memory_view_holder ivars); the
1283
+ outer CAStride is what the user sees, with full strides
1284
+ (including negative) preserved. */
1285
+ if (v->ndim >= 1 && v->format != NULL &&
1286
+ ! ca_mv_view_is_contiguous(v)) {
1287
+ ca_size_t inner_dim[1] = { 1 };
1288
+ volatile VALUE inner_wrap;
1289
+ ca_size_t strides_arr[CA_RANK_MAX];
1290
+ int i;
1291
+
1292
+ inner_wrap = rb_ca_wrap_new(data_type, 1, inner_dim,
1293
+ ca_mv_carray_bytes_for(data_type, v->item_size),
1294
+ NULL, (char *) v->data);
1295
+ rb_ivar_set(inner_wrap, rb_intern("memory_view_source"), obj);
1296
+ holder_obj = TypedData_Wrap_Struct(rb_cObject,
1297
+ &ca_mv_imported_holder_data_type,
1298
+ holder);
1299
+ rb_ivar_set(inner_wrap, rb_intern("memory_view_holder"), holder_obj);
1300
+ if (v->readonly) {
1301
+ CArray *iw;
1302
+ if (ca_mv_extract(inner_wrap, &iw)) {
1303
+ ca_set_flag(iw, CA_FLAG_READ_ONLY);
1304
+ }
1305
+ }
1306
+ for (i = 0; i < ndim; i++) {
1307
+ strides_arr[i] = (ca_size_t) v->strides[i];
1308
+ }
1309
+ {
1310
+ VALUE result =
1311
+ rb_ca_stride_new(inner_wrap, data_type,
1312
+ ca_mv_carray_bytes_for(data_type, v->item_size),
1313
+ ndim, dim, strides_arr, /* base_offset */ 0);
1314
+ if (v->readonly) {
1315
+ CArray *rs;
1316
+ if (ca_mv_extract(result, &rs)) {
1317
+ ca_set_flag(rs, CA_FLAG_READ_ONLY);
1318
+ }
1319
+ }
1320
+ return result;
1321
+ }
1322
+ }
1323
+
1324
+ /* Same struct rb_ca_wrap_new would build, wrapped in the class the
1325
+ receiver asked for. Everything downstream -- the holder and source
1326
+ ivars, the read-only flag, the borrowed mask -- is untouched, which
1327
+ is the point: those are what a gem building its own wrap by hand
1328
+ has to give up. */
1329
+ wrap = ca_wrap_struct_as(
1330
+ ca_wrap_new(data_type, ndim, dim,
1331
+ ca_mv_carray_bytes_for(data_type, v->item_size),
1332
+ NULL, (char *) v->data),
1333
+ target_class);
1334
+
1335
+ rb_ivar_set(wrap, rb_intern("memory_view_source"), obj);
1336
+ holder_obj = TypedData_Wrap_Struct(rb_cObject,
1337
+ &ca_mv_imported_holder_data_type,
1338
+ holder);
1339
+ rb_ivar_set(wrap, rb_intern("memory_view_holder"), holder_obj);
1340
+
1341
+ if (v->readonly) {
1342
+ CArray *ca;
1343
+ if (ca_mv_extract(wrap, &ca)) {
1344
+ ca_set_flag(ca, CA_FLAG_READ_ONLY);
1345
+ }
1346
+ }
1347
+
1348
+ /* Attach borrowed mask: build a CAWrap over the mask buffer and assign
1349
+ it to wrap->mask. Anchor the mask MV's source + holder on the same
1350
+ wrap object so the lifecycle is co-terminal with the data side.
1351
+ When `wrap` is collected: free_ca_wrap recurses into ca->mask (frees
1352
+ the mask wrap struct, leaves its borrowed ptr alone); the holder
1353
+ ivars become unreachable and are GC'd separately, each holder.dfree
1354
+ calling rb_memory_view_release. */
1355
+ if (mask_holder != NULL) {
1356
+ CArray *data_ca, *mask_ca;
1357
+ rb_memory_view_t *mv = &mask_holder->view;
1358
+ VALUE mask_holder_obj;
1359
+ if (! ca_mv_extract(wrap, &data_ca)) {
1360
+ rb_memory_view_release(mv);
1361
+ mask_holder->valid = false;
1362
+ xfree(mask_holder);
1363
+ rb_raise(rb_eRuntimeError,
1364
+ "internal: rb_ca_wrap_new did not return a CArray");
1365
+ }
1366
+ mask_ca = (CArray *) ca_wrap_new(CA_BOOLEAN, ndim, dim, 1, NULL,
1367
+ (char *) mv->data);
1368
+ ca_set_flag(mask_ca, CA_FLAG_MASK_ARRAY);
1369
+ if (mv->readonly) {
1370
+ ca_set_flag(mask_ca, CA_FLAG_READ_ONLY);
1371
+ }
1372
+ data_ca->mask = mask_ca;
1373
+
1374
+ rb_ivar_set(wrap, rb_intern("mask_memory_view_source"), mask_obj);
1375
+ mask_holder_obj = TypedData_Wrap_Struct(rb_cObject,
1376
+ &ca_mv_imported_holder_data_type,
1377
+ mask_holder);
1378
+ rb_ivar_set(wrap, rb_intern("mask_memory_view_holder"), mask_holder_obj);
1379
+ }
1380
+
1381
+ return wrap;
1382
+ }
1383
+
1384
+ /* CArray.from_memory_view(obj, data_type: nil) -- copy into an independent CArray.
1385
+ Accepts strided typed sources (gather copy) and typeless sources
1386
+ (must be 1D contiguous; bytes reinterpreted as `data_type:`). */
1387
+ static VALUE
1388
+ rb_ca_s_from_memory_view (int argc, VALUE *argv, VALUE klass)
1389
+ {
1390
+ rb_memory_view_t view;
1391
+ ca_mv_imported_holder_t *mask_holder = NULL;
1392
+ int8_t data_type, ndim;
1393
+ ca_size_t dim[CA_RANK_MAX];
1394
+ VALUE result, obj, mask_obj;
1395
+ CArray *ca;
1396
+ bool contig;
1397
+ int8_t explicit_data_type =
1398
+ ca_mv_extract_type_and_mask_kwargs(argc, argv,
1399
+ (VALUE *) &obj, (VALUE *) &mask_obj);
1400
+
1401
+ ca_mv_acquire_and_validate(obj, &view, explicit_data_type,
1402
+ &data_type, &ndim, dim);
1403
+
1404
+ /* mask: acquire + validate now so failures clean up the data view too. */
1405
+ if (! NIL_P(mask_obj)) {
1406
+ mask_holder = ca_mv_acquire_mask_view(mask_obj, ndim, dim,
1407
+ NULL, &view);
1408
+ }
1409
+
1410
+ result = rb_carray_new(data_type, ndim, dim,
1411
+ ca_mv_carray_bytes_for(data_type, view.item_size),
1412
+ NULL);
1413
+ if (! ca_mv_extract(result, &ca)) {
1414
+ rb_memory_view_release(&view);
1415
+ rb_raise(rb_eRuntimeError, "internal: rb_carray_new did not return a CArray");
1416
+ }
1417
+
1418
+ /* Typeless producers (format == NULL) and SIMPLE-form producers (shape
1419
+ omitted) are 1-D contiguous by construction; ca_mv_view_is_contiguous
1420
+ handles the NULL shape that would crash the raw contiguity helper. */
1421
+ contig = (view.format == NULL) || ca_mv_view_is_contiguous(&view);
1422
+
1423
+ if (contig) {
1424
+ size_t total_bytes = (size_t) ca_length(ca);
1425
+ /* For typeless producers byte_size and total_bytes are equal by
1426
+ construction; for typed producers we already validated alignment. */
1427
+ if (view.byte_size < (ssize_t) total_bytes) {
1428
+ rb_memory_view_release(&view);
1429
+ if (mask_holder) {
1430
+ rb_memory_view_release(&mask_holder->view);
1431
+ mask_holder->valid = false;
1432
+ xfree(mask_holder);
1433
+ }
1434
+ rb_raise(rb_eRuntimeError,
1435
+ "internal: byte_size mismatch (carray=%lu, view=%ld)",
1436
+ (unsigned long) total_bytes, (long) view.byte_size);
1437
+ }
1438
+ memcpy(ca->ptr, view.data, total_bytes);
1439
+ }
1440
+ else {
1441
+ /* Strided typed source: walk indices in row-major order and gather. */
1442
+ ca_mv_copy_strided_to_contiguous(ca->ptr, &view, ca->elements);
1443
+ }
1444
+
1445
+ rb_memory_view_release(&view);
1446
+
1447
+ /* mask: materialise the canonical CA_BOOLEAN mask slot via ca_create_mask,
1448
+ then memcpy bytes from the (validated row-major contig 1-byte/elem) mask
1449
+ buffer. Source format ? / B / b all use byte values where any non-zero
1450
+ means masked; that semantic matches CArray's mask convention directly
1451
+ when we coerce non-zero to 1. */
1452
+ if (mask_holder) {
1453
+ rb_memory_view_t *mv = &mask_holder->view;
1454
+ size_t n = (size_t) ca->elements;
1455
+ size_t i;
1456
+ const uint8_t *src = (const uint8_t *) mv->data;
1457
+ boolean8_t *dst;
1458
+ ca_create_mask(ca);
1459
+ dst = (boolean8_t *) ca->mask->ptr;
1460
+ for (i = 0; i < n; i++) {
1461
+ dst[i] = (src[i] != 0) ? 1 : 0;
1462
+ }
1463
+ rb_memory_view_release(mv);
1464
+ mask_holder->valid = false;
1465
+ xfree(mask_holder);
1466
+ }
1467
+
1468
+ return result;
1469
+ }
1470
+
1471
+ /* ---------------- Ruby method ---------------- */
1472
+
1473
+ static VALUE
1474
+ rb_ca_s_memory_view_available_p (VALUE klass, VALUE obj)
1475
+ {
1476
+ return rb_memory_view_available_p(obj) ? Qtrue : Qfalse;
1477
+ }
1478
+
1479
+ /* Diagnostic: if memory_view_available? returns false on a CArray,
1480
+ why? Returns nil (no problem detected) or a String explaining
1481
+ the offending link in the parent chain. Useful for users hitting
1482
+ the alias-chain reject who can't tell from the generic
1483
+ "memory view not available" error what specifically went wrong. */
1484
+ static VALUE
1485
+ rb_ca_s_memory_view_reject_reason (VALUE klass, VALUE obj)
1486
+ {
1487
+ CArray *ca;
1488
+ ca_mv_reject_t rej;
1489
+ if (! ca_mv_extract(obj, &ca)) {
1490
+ return rb_str_new_cstr("not a CArray");
1491
+ }
1492
+ if (! ca_mv_ca_exportable(ca, obj)) {
1493
+ if (ca->data_type == CA_FIXLEN && RTEST(rb_ca_has_data_class(obj))) {
1494
+ return rb_str_new_cstr("struct data_class contains a member type the MV "
1495
+ "producer cannot express yet (nested struct, "
1496
+ "CArray template, :fixlen, or :object)");
1497
+ }
1498
+ return rb_sprintf("element data_type (%d) is not exportable via memory_view "
1499
+ "(CA_FIXLEN without data_class / CA_OBJECT)",
1500
+ ca->data_type);
1501
+ }
1502
+ if (ca_has_mask(ca)) {
1503
+ return rb_str_new_cstr("array has a mask; use arr.value (mask-ignoring CARefer) "
1504
+ "or arr.strip_mask(fill) first");
1505
+ }
1506
+ if (! ca_mv_check_alias_chain(ca, &rej)) {
1507
+ const char *where;
1508
+ if (rej.depth == 0) where = "this view";
1509
+ else if (rej.depth == 1) where = "parent";
1510
+ else where = NULL; /* ancestor[N] */
1511
+ if (where) {
1512
+ return rb_sprintf("%s is %s (%s); zero-copy wrap not possible. "
1513
+ "Use CArray.from_memory_view(arr) or arr.to_ca for a snapshot.",
1514
+ where, rej.class_name, rej.reason);
1515
+ } else {
1516
+ return rb_sprintf("ancestor[%d] is %s (%s); zero-copy wrap not possible. "
1517
+ "Use CArray.from_memory_view(arr) or arr.to_ca for a snapshot.",
1518
+ rej.depth - 1, rej.class_name, rej.reason);
1519
+ }
1520
+ }
1521
+ return Qnil;
1522
+ }
1523
+
1524
+ /* Test hook: expose the format-string parser directly so the consumer
1525
+ table (see docs/interop/MemoryViewFormat.md) can be exercised without needing
1526
+ a producer that emits every synonym. Returns the CArray data_type
1527
+ integer on accept, or nil on reject. */
1528
+ static VALUE
1529
+ rb_ca_s_parse_memory_view_format (VALUE klass, VALUE fmt_v, VALUE item_size_v)
1530
+ {
1531
+ const char *fmt = NIL_P(fmt_v) ? NULL : StringValueCStr(fmt_v);
1532
+ ssize_t item_size = (ssize_t) NUM2LL(item_size_v);
1533
+ int8_t dt;
1534
+ if (item_size <= 0) return Qnil;
1535
+ dt = ca_mv_data_type_from_format(fmt, item_size);
1536
+ if (dt < 0) return Qnil;
1537
+ /* Return Symbol so the test hook output compares directly against
1538
+ CA_* (Symbol) constants. */
1539
+ return rb_ca_data_type_to_sym(dt);
1540
+ }
1541
+
1542
+ /* Test hook: return the MV format string the producer would emit
1543
+ for `obj`, without actually building the rb_memory_view_t. Used
1544
+ by spec_ai to verify PEP 3118 struct format emission for CAStruct
1545
+ arrays. Returns nil if obj isn't exportable. */
1546
+ static VALUE
1547
+ rb_ca_s_memory_view_format (VALUE klass, VALUE obj)
1548
+ {
1549
+ CArray *ca;
1550
+ if (! ca_mv_extract(obj, &ca)) return Qnil;
1551
+ if (! ca_mv_ca_exportable(ca, obj)) return Qnil;
1552
+ if (ca->data_type == CA_FIXLEN && RTEST(rb_ca_has_data_class(obj))) {
1553
+ return ca_mv_struct_format_for_data_class(rb_ca_data_class(obj));
1554
+ }
1555
+ if (ca->data_type == CA_FIXLEN) {
1556
+ /* Plain CA_FIXLEN: emit "Ns" via the same caching scheme used by
1557
+ the real producer (ca_mv_get). Reusing the cache keeps the
1558
+ returned String identity-equal to the one the producer hands out
1559
+ via view->format, which test code may rely on. */
1560
+ VALUE fmt_str = rb_attr_get(obj, rb_intern("__mv_fixlen_format__"));
1561
+ if (NIL_P(fmt_str)) {
1562
+ char fmt_buf[32];
1563
+ snprintf(fmt_buf, sizeof(fmt_buf), "%lds", (long) ca->bytes);
1564
+ fmt_str = rb_str_new_cstr(fmt_buf);
1565
+ rb_obj_freeze(fmt_str);
1566
+ rb_ivar_set(obj, rb_intern("__mv_fixlen_format__"), fmt_str);
1567
+ }
1568
+ return fmt_str;
1569
+ }
1570
+ const char *fmt = ca_mv_format_for(ca->data_type);
1571
+ return fmt ? rb_str_new_cstr(fmt) : Qnil;
1572
+ }
1573
+
1574
+ /* ---------------- Init ---------------- */
1575
+
1576
+ static const rb_memory_view_entry_t ca_memory_view_entry = {
1577
+ .get_func = ca_mv_get,
1578
+ .release_func = ca_mv_release,
1579
+ .available_p_func = ca_mv_available_p,
1580
+ };
1581
+
1582
+ void
1583
+ Init_carray_memory_view (void)
1584
+ {
1585
+ /* Statically known classes. */
1586
+ rb_memory_view_register(rb_cCArray, &ca_memory_view_entry);
1587
+ rb_memory_view_register(rb_cCAWrap, &ca_memory_view_entry);
1588
+ rb_memory_view_register(rb_cCScalar, &ca_memory_view_entry);
1589
+ rb_memory_view_register(rb_cCARefer, &ca_memory_view_entry);
1590
+ rb_memory_view_register(rb_cCABlock, &ca_memory_view_entry);
1591
+ rb_memory_view_register(rb_cCASelect, &ca_memory_view_entry);
1592
+ rb_memory_view_register(rb_cCARepeat, &ca_memory_view_entry);
1593
+
1594
+ /* Runtime-assigned classes: resolve via Ruby constants. */
1595
+ for (int i = 0; i < CA_MV_NUM_RUNTIME_OBJ_TYPES; i++) {
1596
+ ca_mv_runtime_type_t *rt = &ca_mv_runtime_types[i];
1597
+ ID class_id = rb_intern(rt->class_name);
1598
+ ID const_id = rb_intern(rt->const_name);
1599
+ if (rb_const_defined(rb_cObject, class_id)) {
1600
+ rt->klass = rb_const_get(rb_cObject, class_id);
1601
+ if (rt->strategy != CA_MV_REJECT) {
1602
+ rb_memory_view_register(rt->klass, &ca_memory_view_entry);
1603
+ }
1604
+ }
1605
+ if (rb_const_defined(rb_cObject, const_id)) {
1606
+ rt->id = NUM2INT(rb_const_get(rb_cObject, const_id));
1607
+ }
1608
+ }
1609
+
1610
+ rb_define_singleton_method(rb_cCArray, "memory_view_available?",
1611
+ rb_ca_s_memory_view_available_p, 1);
1612
+ rb_define_singleton_method(rb_cCArray, "memory_view_reject_reason",
1613
+ rb_ca_s_memory_view_reject_reason, 1);
1614
+ rb_define_singleton_method(rb_cCArray, "from_memory_view",
1615
+ rb_ca_s_from_memory_view, -1);
1616
+ rb_define_singleton_method(rb_cCArray, "wrap_memory_view",
1617
+ rb_ca_s_wrap_memory_view, -1);
1618
+ /* Test hooks (used by spec_ai/test_format_parser.rb and
1619
+ spec_ai/test_mv_struct_format.rb). Private API; names start
1620
+ with __ to discourage external use. */
1621
+ rb_define_singleton_method(rb_cCArray, "__memory_view_parse_format__",
1622
+ rb_ca_s_parse_memory_view_format, 2);
1623
+ rb_define_singleton_method(rb_cCArray, "__memory_view_format__",
1624
+ rb_ca_s_memory_view_format, 1);
1625
+ }