carray 2.0.1 → 3.0.1

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