ndav 0.0.3 → 0.0.5

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 443c009b1751ab850206b12f6ae175989fef5bb3e9dfd6094d069b338262474a
4
- data.tar.gz: fc5ef147dcbe89690add67f9aca9ed11a71a56ed2d801284936753066da2be2c
3
+ metadata.gz: '0648fef27ca47ec51a45cacb90330e5fb2de53071d4223604752c04746ad9c2d'
4
+ data.tar.gz: 4af1dacda9bfa1258552bc271d773136b06ea414cce61c75c194c590f1361850
5
5
  SHA512:
6
- metadata.gz: 7ce73fa38eba65d0c1933cef3e787a6a770854847f7946098ca4ff7e26ba0e588ee6d5a1d79e469abb27a0c303722f69e9030943a73b5295e7a422c73dee96f5
7
- data.tar.gz: 917725d70d4a3269da345d389b93562d54bec9b0911e88229a469a98d79203026ab37bbe834039aded9b1635c827d6723b5b82f23dcacd3864e9df0908dcab20
6
+ metadata.gz: b81a17db0cc3814963b47e590e2f3e021e86792326a98ad1f246df8f887c4f1e49aec42808c82aeceb522e28f05f7dac9d3a6eb65417fbfc9ff785cb75284723
7
+ data.tar.gz: 798ed5c4f2f84fa73b262af460e0054454a78d6f8a9eeacbb191f0b866c6c4e98ae72ed296af23b7a67d98d6894a988cc22dfc23484267d1773bd91ed7043a63
data/.gitignore CHANGED
@@ -6,3 +6,5 @@ ext/Makefile
6
6
  *.o
7
7
  *.bundle.dSYM
8
8
  pkg/
9
+ doc/
10
+ .yardoc/
data/.gitlab-ci.yml CHANGED
@@ -12,3 +12,15 @@ test:
12
12
  - /root/.local/share/gem/ruby
13
13
  script:
14
14
  - rake test
15
+
16
+ pages:
17
+ stage: deploy
18
+ image: ruby:4.0
19
+ script:
20
+ - bundle exec rake yard
21
+ - mv doc public
22
+ artifacts:
23
+ paths:
24
+ - public
25
+ only:
26
+ - main
data/.yardopts ADDED
@@ -0,0 +1,4 @@
1
+ --title="NDAV - N-Dimensional Array View" -M commonmarker
2
+ -
3
+ LICENSE.txt
4
+ lib/ndav/ffi.rb
data/README.md CHANGED
@@ -1,9 +1,183 @@
1
1
  NDAV - N-Dimensional Array View
2
2
  ===============================
3
3
 
4
- Wrapper for MemoryView and pointer.
4
+ [![Gem Version](https://badge.fury.io/rb/ndav.svg)](https://badge.fury.io/rb/ndav)
5
+
6
+ A thin wrapper around [MemoryView][] ("buffer protocol" for Ruby).
7
+
8
+ It provides an interoperability layer for multi-dimensional arrays which can be shared between libraries.
9
+
10
+ ![NDAV converts library data each other](https://i.gyazo.com/905f541f15c0d57db5fc7ea3f6edaf07.png)
11
+
12
+ SYNOPSIS
13
+ --------
14
+
15
+ waveform, sample_rate = TorchAudio.load("path/to/audio.wav")
16
+
17
+ # Convert Torch::Tensor to NDAV
18
+ # so that you can convert it to OrtValue,
19
+ # a data format for ONNX Runtime
20
+ input = waveform
21
+ .to_ndav
22
+ .to_ort_value
23
+
24
+ # Make ONNX Runtime return result as OrtValue
25
+ outputs = OnnxRuntime::Session.new("path/to/model.onnx")
26
+ .run(
27
+ [:output_name],
28
+ {input_name: input},
29
+ output_type: :ort_value
30
+ )
31
+
32
+ # You may convert OrtValue to Torch::Tensor via NDAV
33
+ output_tensor = outputs[0]
34
+ .to_ndav
35
+ .to_torch_tensor # converts back to Torch::Tensor
36
+
37
+ TorchAudio.save("path/to/output.wav", output_tensor, sample_rate)
38
+
39
+ ABSTRACT
40
+ --------
41
+
42
+ NDAV acts as an interoperability layer between multi-dimensional arrays including images, audio and tensors such as [Numo::NArray][], [Torch.rb][]'s `Torch::Tensor`, [ONNX Runtime Ruby][]'s `OnnxRuntime::OrtValue`, [Red Arrow][]'s `Arrow::Array` and so on.
43
+
44
+ It allows data to be shared without copying.
45
+
46
+ BACKGROUND
47
+ ----------
48
+
49
+ In the modern Ruby community, [Numo::NArray][] is often used for data conversion. But, data are copied when converting to and from Numo::NArray. In addition, Numo::NArray neither exports nor accepts MemoryView.
50
+
51
+ [Red Arrow][] is also used and it can export MemoryView from `Arrow::Array` (not from `Arrow::Tensor`, though). It also can be converted to and from Numo::NArray using [Red Arrow Numo::NArray][]. But, in real-world usage, we need, for example, to convert data with many hops:
52
+
53
+ Torch::Tensor -> Numo::NArray -> Red Arrow -> MemoryView -> some process...
54
+
55
+ It might not be difficult, but a little bit cumbersome. Additionally, Red Arrow doesn't accept MemoryView.
56
+
57
+ USAGE
58
+ -----
59
+
60
+ `ndav` gem is just a base library. You need to install bridges as well. Say, assume you want to make conversions between Numo::NArray each other.
61
+
62
+ require "numo/narray"
63
+ require "ndav"
64
+ require "ndav/numo/narray"
65
+
66
+ numo = Numo::SFloat.new(3, 5).seq # => Numo::SFloat
67
+ ndav = numo.to_ndav # => NDAV
68
+
69
+ numo = Numo::SFloat.from_ndav(ndav) # => Numo::SFloat
70
+ ndav = NDAV.from_numo_narray(numo) # => NDAV
71
+
72
+ include NDAV::Converter
73
+ numo = NumoNArray(ndav) # => Numo::SFloat
74
+ ndav = NDAV(numo) # => NDAV
75
+
76
+ For `Torch::Tensor` and `OnnxRuntime::OrtValue`, you can do the same operation, therefore you may convert them to each other, like this:
77
+
78
+ numo
79
+ .to_ndav
80
+ .to_torch_tensor
81
+ .then {|torch_tensor| some_process(torch_tensor)}
82
+ .to_ndav
83
+ .to_ort_value
84
+ .then {|ort_value|
85
+ OnnxRuntime::Session.new("model.onnx")
86
+ .run(
87
+ [:output],
88
+ {input: ort_value},
89
+ output_type: :ort_value
90
+ )[0]
91
+ }
92
+ .to_ndav
93
+ .to_torch_tensor
94
+ .then {|torch_tensor| TorchAudio.save(torch_tensor, sample_rate)}
95
+
96
+ ### Working With MemoryView ###
97
+
98
+ NDAV can be initialized *directly* from libraries which export [MemoryView][] such as [Red Arrow][], without any bridge library:
99
+
100
+ arrow = Arrow::Int16Array.new([1, 2, 3])
101
+ ndav = NDAV.new(arrow)
102
+
103
+ On the other hand, it also exports MemoryView. You can pass NDAV arrays *directly* to methods which accept MemoryView such as [whispercpp][] without bridge libraries:
104
+
105
+ waveform, sample_rate = TorchAudio.load("path/to/audio.wav")
106
+ samples = waveform.to_ndav
107
+ whisper.full(params, samples)
108
+
109
+ ### Notice On Memory Sharing ###
110
+
111
+ Notice that NDAV is just a memory view and libraries share a memory address. If you change source data destructively, it affects converted data.
112
+
113
+ Additionally, you potentially encounter odd data corruption or segmentation fault. These might be bugs in bridge libraries such as [ndav-numo-narray][]. As a user, you don't need to worry about this kind of memory management, but it's worth knowing such situations may occur.
114
+
115
+ INSTALLATION
116
+ ------------
117
+
118
+ % gem install ndav
119
+
120
+ or,
121
+
122
+ % bundle add ndav
123
+
124
+ But, you need bridges for real-world use. See each bridge's documentation for individual requirements.
125
+
126
+ BRIDGES
127
+ -------
128
+
129
+ There are some bridges using NDAV:
130
+
131
+ * {file:ndav/ffi ndav/ffi}: [`FFI::MemoryPointer`][FFI Pointer], [`FFI::Pointer`][FFI Pointer] <-> `NDAV`
132
+ * [ndav-numo-narray][]: [`Numo::NArray`][Numo::NArray] <-> `NDAV`
133
+ * [ndav-ort_value][]: [`OnnxRuntime::OrtValue`][ONNX Runtime Ruby] <-> `NDAV`
134
+ * [ndav-torch-tensor][]: [`Torch::Tensor`][Torch.rb] <-> `NDAV`
135
+ * [onnxruntime-torch-tensor][]: [`Torch::Tensor`][Torch.rb] <-> [`OnnxRuntime::OrtValue`][ONNX Runtime Ruby] via `NDAV`
136
+
137
+ APPLICATIONS AND LIBRARIES WHICH USE NDAV
138
+ -----------------------------------------
139
+
140
+ * [GTCRN][] - An audio speech enhancement (noise reduction) library. It uses [TorchAudio][] for loading and preprocessing audio before passing it to ONNX Runtime, then performs post-processing on the result and writes it back to a file.
141
+ * [Itak][] - An audio processing tool for podcasters. After reducing noise with the GTCRN mentioned above, it uses the VAD function of [whispercpp][] to remove silent periods. Although whispercpp does not accept existing tensor libraries, it does accept MemoryView, so it can be passed via NDAV.
142
+
143
+ CREATING BRIDGES
144
+ ----------------
145
+
146
+ Refer to existing bridge implementations listed above to create your bridge.
147
+
148
+ The points are:
149
+
150
+ * Implement `FromNDAV#from_ndav`, `ToNDAV#to_ndav`, {NDAV.register register} them, and `NDAV.from_your_data` and `NDAV#to_your_data` are automatically derived
151
+ * When initializing NDAV object from your object, use `lifetime` keyword argument for {NDAV#initialize} effectively to prevent Ruby from GCing your object, which would lead to a dangling pointer
152
+ * When initializing your object from NDAV object, keep NDAV object alive to prevent Ruby from GCing NDAV object, which would lead to a dangling pointer, [ndav-numo-narray][], for instance, embeds the NDAV object in an instance variable
153
+
154
+ An advantage of NDAV over raw MemoryView is that you can write bridges in pure Ruby in most cases. It helps prototyping and experimentation. As an exception, I had to write C code for ndav-numo-narray because Numo::NArray only provides methods that access data by copying and does not directly expose its raw data pointer to Ruby API. However, in even such case, pure Ruby bridge remains a viable option for prototyping and experiments where a single initial copy is acceptable.
155
+
156
+ If you are a library author, I want you to consider making your library work with [MemoryView][] instead of creating an NDAV bridge.
157
+
158
+ FUTURE
159
+ ------
160
+
161
+ If [MemoryView][] gets popular enough in the Ruby ecosystem, this library will end its role and will no longer be needed. I hope such future.
5
162
 
6
163
  LICENSE
7
164
  -------
8
165
 
9
- BSD-2-Clause license. See LICENSE.txt file.
166
+ BSD-2-Clause license. See {file:LICENSE.txt} file.
167
+
168
+ [MemoryView]: https://docs.ruby-lang.org/en/master/contributing/memory_view_md.html
169
+ [Numo::NArray]: https://ruby-numo.github.io/narray/
170
+ [Torch.rb]: https://github.com/ankane/torch.rb
171
+ [ONNX Runtime Ruby]: https://github.com/ankane/onnxruntime-ruby
172
+ [Red Arrow]: https://github.com/apache/arrow/tree/main/ruby
173
+ [Red Arrow Numo::NArray]: https://github.com/red-data-tools/red-arrow-numo-narray
174
+ [ndav/ffi]: https://gitlab.com/KitaitiMakoto/ndav/-/blob/main/lib/ndav/ffi.rb
175
+ [FFI Pointer]: https://github.com/ffi/ffi/wiki/Pointers
176
+ [whispercpp]: https://github.com/ggml-org/whisper.cpp/tree/master/bindings/ruby
177
+ [ndav-numo-narray]: https://gitlab.com/KitaitiMakoto/ndav-numo-narray
178
+ [ndav-ort_value]: https://gitlab.com/KitaitiMakoto/ndav-ort_value
179
+ [ndav-torch-tensor]: https://gitlab.com/KitaitiMakoto/ndav-torch-tensor
180
+ [onnxruntime-torch-tensor]: https://gitlab.com/KitaitiMakoto/onnxruntime-torch-tensor
181
+ [GTCRN]: https://gitlab.com/KitaitiMakoto/gtcrn
182
+ [TorchAudio]: https://github.com/ankane/torchaudio-ruby
183
+ [Itak]: https://gitlab.com/KitaitiMakoto/itak
data/Rakefile CHANGED
@@ -1,18 +1,20 @@
1
1
  require "rake/clean"
2
2
  require "rake/testtask"
3
3
  require "rubygems/tasks"
4
+ require "yard"
4
5
 
5
6
  DL_NAME = "ndav".ext(RbConfig::CONFIG["DLEXT"])
6
7
  DL_BUILD_PATH = File.join("ext", DL_NAME)
7
8
  DL_PATH = File.join("lib", DL_NAME)
8
9
 
9
10
  SRC = FileList["ext/**.{h,c,rb}"]
11
+ CLEAN.include SRC.select {|src| src.end_with?(".o", ".so", ".bundle", ".dll") }
10
12
 
11
13
  task default: :test
12
14
 
13
15
  Rake::TestTask.new test: DL_PATH
14
- tasks = Gem::Tasks.new
15
- gemspec = tasks.build.gem.project.gemspec
16
+ Gem::Tasks.new
17
+ YARD::Rake::YardocTask.new
16
18
 
17
19
  file DL_PATH => DL_BUILD_PATH do |t|
18
20
  copy t.source, t.name
data/ext/ndav.c CHANGED
@@ -1,22 +1,34 @@
1
- #include "ndav.h"
1
+ #include <ruby.h>
2
+ #include <ruby/memory_view.h>
3
+
4
+ static ID id_ndav_validated_descriptor;
5
+ static VALUE sym_data;
6
+ static VALUE sym_shape;
7
+ static VALUE sym_strides;
8
+ static VALUE sym_format;
9
+ static VALUE sym_byte_size;
10
+ static VALUE sym_readonly_p;
11
+ static VALUE sym_sub_offsets;
12
+ static VALUE sym_ndim;
13
+ static VALUE sym_item_size;
14
+ static VALUE sym_row_major_contiguous_p;
15
+ static VALUE sym_column_major_contiguous_p;
16
+
17
+ typedef struct {
18
+ VALUE descriptor;
19
+ } private_data_t;
2
20
 
3
- ID id_to_s;
4
-
5
- typedef struct fill_size_array_args_t {
6
- VALUE src;
7
- ssize_t *dest;
8
- ssize_t size;
9
- } fill_size_array_args_t;
10
-
11
- static VALUE
12
- fill_size_array(VALUE rb_args)
21
+ static bool
22
+ fill_size_array(VALUE src, ssize_t *dest, ssize_t size)
13
23
  {
14
- struct fill_size_array_args_t *args = (fill_size_array_args_t *)rb_args;
15
- for (ssize_t i = 0; i < args->size; i++) {
16
- VALUE val = rb_ary_entry(args->src, i);
17
- args->dest[i] = NUM2SSIZET(val);
24
+ for (ssize_t i = 0; i < size; i++) {
25
+ VALUE val = rb_ary_entry(src, i);
26
+ if (!RB_INTEGER_TYPE_P(val)) {
27
+ return false;
28
+ }
29
+ dest[i] = NUM2SSIZET(val);
18
30
  }
19
- return Qnil;
31
+ return true;
20
32
  }
21
33
 
22
34
  static bool
@@ -25,24 +37,38 @@ ndav_get_memory_view(const VALUE ndav, rb_memory_view_t *view, int flags)
25
37
  bool row_major_requested = (flags & RUBY_MEMORY_VIEW_ROW_MAJOR) == RUBY_MEMORY_VIEW_ROW_MAJOR;
26
38
  bool column_major_requested = (flags & RUBY_MEMORY_VIEW_COLUMN_MAJOR) == RUBY_MEMORY_VIEW_COLUMN_MAJOR;
27
39
  bool indirect_requested = (flags & RUBY_MEMORY_VIEW_INDIRECT) == RUBY_MEMORY_VIEW_INDIRECT;
28
- if ((column_major_requested && !row_major_requested) || indirect_requested) {
40
+ VALUE desc = rb_funcall(ndav, id_ndav_validated_descriptor, 1, INT2NUM(flags));
41
+ if (!RB_TYPE_P(desc, T_HASH)) {
42
+ rb_warn("descriptor not Hash");
43
+ return false;
44
+ }
45
+ bool row_major_contiguous = rb_hash_aref(desc, sym_row_major_contiguous_p);
46
+ bool column_major_contiguous = rb_hash_aref(desc, sym_column_major_contiguous_p);
47
+ if (row_major_requested && column_major_requested) { // row-major OR column-major requested
48
+ if (!row_major_contiguous && !column_major_contiguous) {
49
+ return false;
50
+ }
51
+ }
52
+ // Currently, non-contiguous array not supported
53
+ else if (row_major_requested && !row_major_contiguous) {
54
+ return false;
55
+ }
56
+ // Currently, non-contiguous array not supported
57
+ else if (column_major_requested && !column_major_contiguous) {
29
58
  return false;
30
59
  }
31
60
  bool writable_requested = (flags & RUBY_MEMORY_VIEW_WRITABLE) == RUBY_MEMORY_VIEW_WRITABLE;
32
61
  // TODO: Handle flags
33
62
 
34
- VALUE readonly = rb_iv_get(ndav, "@readonly");
35
- if (NIL_P(readonly)) {
36
- if (writable_requested) {
37
- rb_warn("not writable");
38
- return false;
39
- }
40
- view->readonly = true;
41
- } else {
42
- view->readonly = RTEST(readonly);
63
+ VALUE readonly_v = rb_hash_aref(desc, sym_readonly_p);
64
+ bool readonly = NIL_P(readonly_v) || RTEST(readonly_v);
65
+ if (writable_requested && readonly) {
66
+ rb_warn("not writable");
67
+ return false;
43
68
  }
69
+ view->readonly = readonly;
44
70
  view->obj = ndav;
45
- VALUE val = rb_funcall(ndav, id_to_s, 0);
71
+ VALUE val = rb_hash_aref(desc, sym_data);
46
72
  char *data = StringValuePtr(val);
47
73
  view->data = data;
48
74
  // TODO: Commonalize
@@ -50,59 +76,91 @@ ndav_get_memory_view(const VALUE ndav, rb_memory_view_t *view, int flags)
50
76
  // * Use TypedData?
51
77
  // * If so, calling attr readers at Ruby layer has performance disadvantage.
52
78
  // * Freezing instance vars in #initialize and then embed them to TypedData may be a solution
53
- VALUE item_size = rb_iv_get(ndav, "@item_size");
79
+ VALUE item_size = rb_hash_aref(desc, sym_item_size);
54
80
  view->item_size = NUM2SSIZET(item_size);
55
- VALUE byte_size = rb_iv_get(ndav, "@byte_size");
81
+ VALUE byte_size = rb_hash_aref(desc, sym_byte_size);
56
82
  view->byte_size = NUM2SSIZET(byte_size);
57
- VALUE ndim = rb_iv_get(ndav, "@ndim");
83
+ VALUE ndim = rb_hash_aref(desc, sym_ndim);
58
84
  view->ndim = NUM2SSIZET(ndim);
59
- VALUE format = rb_iv_get(ndav, "@format");
85
+ VALUE format = rb_hash_aref(desc, sym_format);
60
86
  view->format = StringValueCStr(format);
61
87
 
62
- int state;
63
-
64
88
  // TODO: Commonalize
65
- VALUE shape = rb_iv_get(ndav, "@shape");
89
+ VALUE shape = rb_hash_aref(desc, sym_shape);
66
90
  if (!RB_TYPE_P(shape, T_ARRAY)) {
67
91
  rb_warn("@shape is not an array");
68
92
  return false;
69
93
  }
70
- ssize_t *view_shape = ALLOC_N(ssize_t, view->ndim);
71
- fill_size_array_args_t shape_args = {
72
- shape,
73
- view_shape,
74
- view->ndim,
75
- };
76
- rb_protect(fill_size_array, (VALUE)&shape_args, &state);
77
- if (state) {
78
- xfree(view_shape);
79
- rb_jump_tag(state);
94
+ ssize_t *view_shape = (ssize_t *)malloc(sizeof(ssize_t) * view->ndim);
95
+ if (!view_shape) {
96
+ return false;
97
+ }
98
+ if (!fill_size_array(shape, view_shape, view->ndim)) {
99
+ free((void *)view_shape);
80
100
  return false;
81
101
  }
82
102
  view->shape = view_shape;
83
103
 
84
- VALUE strides = rb_iv_get(ndav, "@strides");
104
+ VALUE strides = rb_hash_aref(desc, sym_strides);
85
105
  if (!RB_TYPE_P(strides, T_ARRAY)) {
86
106
  rb_warn("@strides is not an array");
87
- xfree((void *)view->shape);
107
+ free((void *)view->shape);
88
108
  return false;
89
109
  }
90
- ssize_t *view_strides = ALLOC_N(ssize_t, view->ndim);
91
- fill_size_array_args_t strides_args = {
92
- strides,
93
- view_strides,
94
- view->ndim
95
- };
96
- rb_protect(fill_size_array, (VALUE)&strides_args, &state);
97
- if (state) {
98
- xfree((void *)view_shape);
99
- xfree((void *)view_strides);
100
- rb_jump_tag(state);
110
+ ssize_t *view_strides = (ssize_t *)malloc(sizeof(ssize_t) * view->ndim);
111
+ if (!view_strides) {
112
+ free((void *)view->shape);
113
+ return false;
114
+ }
115
+ if (!fill_size_array(strides, view_strides, view->ndim)) {
116
+ free((void *)view->shape);
117
+ free((void *)view_strides);
101
118
  return false;
102
119
  }
103
120
  view->strides = view_strides;
104
121
 
105
- view->sub_offsets = NULL;
122
+ VALUE sub_offsets = rb_hash_aref(desc, sym_sub_offsets);
123
+ if (NIL_P(sub_offsets)) {
124
+ if (indirect_requested) {
125
+ rb_warn("indirect requested but sub_offsets is NULL");
126
+ free((void *)view->shape);
127
+ free((void *)view->strides);
128
+ return false;
129
+ }
130
+ view->sub_offsets = NULL;
131
+ } else if (!RB_TYPE_P(sub_offsets, T_ARRAY)) {
132
+ rb_warn("sub_offsets is not an array");
133
+ free((void *)view->shape);
134
+ free((void *)view->strides);
135
+ return false;
136
+ } else {
137
+ ssize_t *view_sub_offsets = (ssize_t *)malloc(sizeof(ssize_t) * view->ndim);
138
+ if (!view_sub_offsets) {
139
+ free((void *)view->shape);
140
+ free((void *)view->strides);
141
+ return false;
142
+ }
143
+ if (!fill_size_array(sub_offsets, view_sub_offsets, view->ndim)) {
144
+ free((void *)view->shape);
145
+ free((void *)view->strides);
146
+ free((void *)view_sub_offsets);
147
+ return false;
148
+ }
149
+ view->sub_offsets = view_sub_offsets;
150
+ }
151
+
152
+ private_data_t *private_data = malloc(sizeof(private_data_t));
153
+ if (!private_data) {
154
+ free((void *)view->shape);
155
+ free((void *)view->strides);
156
+ free((void *)view->sub_offsets);
157
+ rb_warn("failed to alloc private_data");
158
+ return false;
159
+ }
160
+ private_data->descriptor = Qnil;
161
+ rb_gc_register_address(&private_data->descriptor);
162
+ private_data->descriptor = desc;
163
+ view->private_data = private_data;
106
164
 
107
165
  return true;
108
166
  }
@@ -111,13 +169,23 @@ static bool
111
169
  ndav_release_memory_view(const VALUE ndav, rb_memory_view_t *view)
112
170
  {
113
171
  if (view->shape) {
114
- xfree((void *)view->shape);
172
+ free((void *)view->shape);
115
173
  view->shape = NULL;
116
174
  }
117
175
  if (view->strides) {
118
- xfree((void *)view->strides);
176
+ free((void *)view->strides);
119
177
  view->strides = NULL;
120
178
  }
179
+ if (view->sub_offsets) {
180
+ free((void *)view->sub_offsets);
181
+ view->sub_offsets = NULL;
182
+ }
183
+ if (view->private_data) {
184
+ private_data_t *private_data = (private_data_t *)view->private_data;
185
+ rb_gc_unregister_address(&private_data->descriptor);
186
+ free(private_data);
187
+ view->private_data = NULL;
188
+ }
121
189
 
122
190
  return true;
123
191
  }
@@ -125,8 +193,8 @@ ndav_release_memory_view(const VALUE ndav, rb_memory_view_t *view)
125
193
  static bool
126
194
  ndav_memory_view_available_p(const VALUE obj)
127
195
  {
128
- VALUE fmv = rb_iv_get(obj, "@fmv");
129
- return !NIL_P(fmv);
196
+ VALUE descriptor = rb_funcall(obj, id_ndav_validated_descriptor, 0);
197
+ return RTEST(descriptor);
130
198
  }
131
199
 
132
200
  const struct rb_memory_view_entry ndav_view_entry = {
@@ -135,11 +203,34 @@ const struct rb_memory_view_entry ndav_view_entry = {
135
203
  ndav_memory_view_available_p
136
204
  };
137
205
 
206
+ static VALUE
207
+ ndav_memory_viewable_s_register(VALUE mod, VALUE klass)
208
+ {
209
+ if (!rb_memory_view_register(klass, &ndav_view_entry)) {
210
+ // error message output in rb_memory_view_register()
211
+ rb_raise(rb_eArgError, "");
212
+ }
213
+
214
+ return Qtrue;
215
+ }
216
+
138
217
  void
139
218
  Init_ndav(void)
140
219
  {
141
- id_to_s = rb_intern("to_s");
220
+ id_ndav_validated_descriptor = rb_intern("ndav_validated_descriptor");
221
+ sym_data = ID2SYM(rb_intern("data"));
222
+ sym_shape = ID2SYM(rb_intern("shape"));
223
+ sym_strides = ID2SYM(rb_intern("strides"));
224
+ sym_format = ID2SYM(rb_intern("format"));
225
+ sym_byte_size = ID2SYM(rb_intern("byte_size"));
226
+ sym_readonly_p = ID2SYM(rb_intern("readonly?"));
227
+ sym_sub_offsets = ID2SYM(rb_intern("sub_offsets"));
228
+ sym_ndim = ID2SYM(rb_intern("ndim"));
229
+ sym_item_size = ID2SYM(rb_intern("item_size"));
230
+ sym_row_major_contiguous_p = ID2SYM(rb_intern("row_major_contiguous?"));
231
+ sym_column_major_contiguous_p = ID2SYM(rb_intern("column_major_contiguous?"));
142
232
 
143
233
  VALUE cNDAV = rb_define_class("NDAV", rb_cObject);
144
- rb_memory_view_register(cNDAV, &ndav_view_entry);
234
+ VALUE mMemoryViewable = rb_define_module_under(cNDAV, "MemoryViewable");
235
+ rb_define_singleton_method(mMemoryViewable, "register", ndav_memory_viewable_s_register, 1);
145
236
  }
@@ -4,6 +4,10 @@ class NDAV
4
4
  alias from_fiddle_memory_view new
5
5
  alias from_fiddle_pointer new
6
6
 
7
+ def from_string(str, *, **)
8
+ new(Fiddle::Pointer[str], *, **)
9
+ end
10
+
7
11
  def register(cls, mdl, name:)
8
12
  if mdl.const_defined?(:FromNDAV)
9
13
  cls.extend mdl::FromNDAV
@@ -13,6 +17,19 @@ class NDAV
13
17
  end
14
18
  end
15
19
 
20
+ if mdl.const_defined?(:MemoryViewable)
21
+ cls.include ::NDAV::MemoryViewable
22
+ cls.include mdl::MemoryViewable
23
+
24
+ unless mdl.const_defined?(:ToNDAV)
25
+ mdl.const_set(:ToNDAV, Module.new {
26
+ def to_ndav(lifetime: self, **)
27
+ ::NDAV.new(self, lifetime:, **)
28
+ end
29
+ })
30
+ end
31
+ end
32
+
16
33
  if mdl.const_defined?(:ToNDAV)
17
34
  cls.include mdl::ToNDAV
18
35
 
data/lib/ndav/ffi.rb CHANGED
@@ -26,6 +26,25 @@ class NDAV
26
26
  end
27
27
  end
28
28
 
29
+ module MemoryViewable
30
+ def ndav_descriptor(**)
31
+ shape = [size / type_size]
32
+ format = TYPE_SIZE_TO_FORMAT[type_size]
33
+ item_size = ITEM_SIZES[format]
34
+ byte_size = shape.reduce(item_size, :*)
35
+ ptr = ::Fiddle::Pointer.new(address, byte_size)
36
+
37
+ {
38
+ data: Fiddle::MemoryView.new(ptr).to_s,
39
+ shape:,
40
+ strides: ::NDAV.default_strides(shape:, item_size:),
41
+ format:,
42
+ byte_size:,
43
+ readonly?: true
44
+ }
45
+ end
46
+ end
47
+
29
48
  module Converter
30
49
  def FFIMemoryPointer(array, *, **)
31
50
  if ::FFI::MemoryPointer === array
data/lib/ndav/flags.rb ADDED
@@ -0,0 +1,26 @@
1
+ class NDAV
2
+ module Flags
3
+ FLAGS = {}
4
+ FLAGS[:simple] = 0
5
+ FLAGS[:writable] = (1<<0)
6
+ FLAGS[:format] = (1<<1)
7
+ FLAGS[:multi_dimensional] = (1<<2)
8
+ FLAGS[:strides] = (1<<3) | FLAGS[:multi_dimensional]
9
+ FLAGS[:row_major] = (1<<4) | FLAGS[:strides]
10
+ FLAGS[:column_major] = (1<<5) | FLAGS[:strides]
11
+ FLAGS[:any_contiguous] = FLAGS[:row_major] | FLAGS[:column_major]
12
+ FLAGS[:indirect] = (1<<6) | FLAGS[:strides]
13
+
14
+ module_function
15
+
16
+ def decode(flags)
17
+ FLAGS.each_with_object({}) {|(name, value), state|
18
+ if name == :simple
19
+ state[name] = flags == 0
20
+ else
21
+ state[name] = flags & value == value
22
+ end
23
+ }
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,47 @@
1
+ class NDAV
2
+ module MemoryViewable
3
+ class << self
4
+ def included(base)
5
+ ::NDAV::MemoryViewable.register base
6
+ end
7
+ end
8
+
9
+ private
10
+
11
+ # Overwrite this method or define each ndav_xxx method
12
+ def ndav_descriptor(simple: true, writable: false, format: false, multi_dimensional: false, strides: false, row_major: false, column_major: false, any_contiguous: false, indirect: false)
13
+ {
14
+ data: ndav_data,
15
+ shape: ndav_shape,
16
+ strides: ndav_strides,
17
+ format: ndav_format,
18
+ byte_size: ndav_byte_size,
19
+ readonly?: ndav_readonly?,
20
+ sub_offsets: ndav_sub_offsets
21
+ }
22
+ end
23
+
24
+ def ndav_validated_descriptor(flags = ::NDAV::Flags::FLAGS[:simple])
25
+ desc = ndav_descriptor(**NDAV::Flags.decode(flags)).to_h
26
+ unless desc.kind_of? Hash
27
+ warn "descriptor not Hash"
28
+ return false
29
+ end
30
+ [:data, :shape, :strides, :format, :byte_size, :readonly?].each do |key|
31
+ unless desc.key? key
32
+ warn ":#{key} not in descriptor"
33
+ return false
34
+ end
35
+ end
36
+ desc.merge(
37
+ ndim: desc[:shape].length,
38
+ item_size: NDAV::ITEM_SIZES[desc[:format]],
39
+ row_major_contiguous?: ::NDAV.row_major_contiguous?(**desc),
40
+ column_major_contiguous?: ::NDAV.column_major_contiguous?(**desc)
41
+ )
42
+ rescue => err
43
+ warn err
44
+ return false
45
+ end
46
+ end
47
+ end
data/lib/ndav.rb CHANGED
@@ -1,88 +1,143 @@
1
1
  require "fiddle"
2
2
  require "ndav.so"
3
+ require "ndav/flags"
3
4
  require "ndav/converter"
5
+ require "ndav/memory_viewable"
4
6
 
5
7
  # TODO: Manage flags of internal data object
6
8
  class NDAV
9
+ include MemoryViewable
10
+
7
11
  # TODO: Want to use return value of rb_memory_view_parse_item_format
8
12
  ITEM_SIZES = ["c", "C", "s", "S", "l", "L", "q", "Q", "f", "d", "s!", "S!", "n", "v", "i", "i!", "I", "I!", "l!", "L!", "N", "V", "e", "g", "q!", "Q!", "E", "G"].collect {|format|
9
13
  [format, [0].pack(format).bytesize]
10
14
  }.to_h
11
15
 
12
- attr_reader :ndim, :shape, :format, :item_size, :byte_size, :strides, :lifetime
16
+ attr_reader :ndim, :shape, :format, :item_size, :byte_size, :strides, :to_s, :lifetime
17
+
18
+ class << self
19
+ def default_strides(shape:, item_size:, row_major: true)
20
+ each_shape = row_major ? shape.reverse_each : shape.each
21
+ each_shape.reduce([[], item_size]) {|(strides, stride), s|
22
+ if row_major
23
+ strides.unshift stride
24
+ else
25
+ strides << stride
26
+ end
27
+ [strides, stride * s]
28
+ }[0]
29
+ end
30
+
31
+ # Algorithm stolen from memory_view.c
32
+ def row_major_contiguous?(format:, shape:, strides:, **)
33
+ ndim = shape.length
34
+ n = ITEM_SIZES[format]
35
+ return strides[0] == n if ndim == 1
36
+
37
+ (ndim - 1).downto 0 do |i|
38
+ return false unless strides[i] == n
39
+
40
+ n *= shape[i]
41
+ end
42
+
43
+ true
44
+ end
45
+
46
+ # Algorithm stolen from memory_view.c
47
+ def column_major_contiguous?(format:, shape:, strides:, **)
48
+ ndim = shape.length
49
+ n = ITEM_SIZES[format]
50
+ return strides[0] == n if ndim == 1
51
+
52
+ 0.upto(ndim - 1) do |i|
53
+ return false unless strides[i] == n
54
+
55
+ n *= shape[i]
56
+ end
57
+
58
+ true
59
+ end
60
+ end
13
61
 
14
62
  # TODO: More options to trick for the case of MemoryView export is wrong but developer don't fix it
15
- def initialize(input, shape: nil, format: nil, lifetime: nil)
16
- @fmv = input.kind_of?(::Fiddle::MemoryView) ? input : ::Fiddle::MemoryView.new(input)
17
- @lifetime = [@fmv.obj, lifetime].compact
18
- @lifetime = nil if @lifetime.empty?
63
+ def initialize(obj, shape: nil, strides: nil, format: nil, lifetime: nil)
64
+ fmv = obj.kind_of?(::Fiddle::MemoryView) ? obj : ::Fiddle::MemoryView.new(obj)
65
+ # Fiddle::MemoryView#to_s is zero-copy
66
+ # Fiddle::MemoryView#to_s embeds obj into the String, so we can prevent GC from collecting it
67
+ @to_s = fmv.to_s
68
+ @lifetime = lifetime
19
69
 
20
- @format = format || @fmv.format
70
+ @format = format || fmv.format
21
71
  raise ArgumentError, %Q|unsupported format: #{@format.to_s.dump}, currently supported: #{ITEM_SIZES.keys}| unless ITEM_SIZES.key?(@format)
22
72
 
23
73
  item_size_by_format = ITEM_SIZES[@format]
24
74
 
25
- @byte_size = @fmv.byte_size
26
- @item_size = input.kind_of?(::Fiddle::Pointer) ? item_size_by_format : @fmv.item_size || item_size_by_format
27
- @shape = shape || @fmv.shape
75
+ @byte_size = fmv.byte_size
76
+ @item_size = obj.kind_of?(::Fiddle::Pointer) ? item_size_by_format : fmv.item_size || item_size_by_format
77
+ @shape = shape || fmv.shape
28
78
  case [@shape, @byte_size]
29
79
  in [nil, nil]
30
- raise ArgumentError, "either shape or byte_size must be present in input or arguments"
31
- in [*, nil]
80
+ raise ArgumentError, "either shape or byte_size must be present in obj or arguments"
81
+ in [_, nil]
32
82
  @byte_size = @shape.reduce(@item_size, :*)
33
- in [nil, *]
83
+ in [nil, _]
34
84
  n, r = @byte_size.divmod(@item_size)
35
85
  raise ArgumentError, "byte_size must be n-times of item_size" unless r.zero?
36
86
  @shape = [n]
37
87
  else
38
88
  # noop
39
89
  end
40
- @strides = @fmv.strides || @shape.reverse_each.reduce([[], @item_size]) {|(strides, stride), s|
41
- strides.unshift(stride)
42
- [strides, stride * s]
43
- }[0]
44
90
  @ndim = @shape.size
45
- @readonly = @fmv.readonly?
91
+ @readonly = fmv.readonly?
92
+ @strides = strides || fmv.strides || self.class.default_strides(shape: @shape, item_size: @item_size)
93
+ @sub_offsets = fmv.sub_offsets
94
+ @row_major_contiguous = self.class.row_major_contiguous?(format: @format, shape: @shape, strides: @strides)
95
+ @column_major_contiguous = self.class.column_major_contiguous?(format: @format, shape: @shape, strides: @strides)
46
96
 
47
- validate
97
+ validate fmv
48
98
  end
49
99
 
50
100
  def readonly?
51
101
  @readonly
52
102
  end
53
103
 
54
- # Zero-copy
55
- def to_s
56
- # MemoryView#to_s is zero-copy
57
- # Assign to @data to keep reference to the string to prevent GC
58
- @data ||= @fmv.to_s
104
+ def row_major_contiguous?
105
+ @row_major_contiguous
106
+ end
107
+
108
+ def column_major_contiguous?
109
+ @column_major_contiguous
59
110
  end
60
111
 
61
112
  def to_ptr
62
- ::Fiddle::Pointer[to_s]
113
+ ::Fiddle::Pointer[@to_s]
63
114
  end
64
115
  alias to_fiddle_pointer to_ptr
65
116
 
66
117
  private
67
118
 
68
- def validate
119
+ def ndav_descriptor(**)
120
+ {
121
+ data: @to_s,
122
+ shape: @shape,
123
+ strides: @strides,
124
+ format: @format,
125
+ byte_size: @byte_size,
126
+ readonly?: @readonly,
127
+ sub_offsets: @sub_offsets
128
+ }
129
+ end
130
+
131
+ def validate(fmv)
69
132
  unless @item_size == ITEM_SIZES[@format]
70
133
  raise ArgumentError, "item_size does not match format"
71
134
  end
72
135
 
73
- if @fmv.sub_offsets
74
- raise ArgumentError, "suboffsets not supported"
75
- end
76
-
77
- stride = @item_size
78
- @shape.reverse_each.with_index do |s, i|
79
- unless @strides[@ndim - 1 - i] == stride
80
- raise ArgumentError, "only row-major contiguous strides is supported"
136
+ if @row_major_contiguous || @column_major_contiguous
137
+ byte_size = @shape.reduce(@item_size, :*)
138
+ unless byte_size == @byte_size
139
+ raise ArgumentError, "strides not match byte_size"
81
140
  end
82
- stride *= s
83
- end
84
- unless stride == @byte_size
85
- raise ArgumentError, "strides not match byte_size"
86
141
  end
87
142
  end
88
143
  end
data/ndav.gemspec CHANGED
@@ -1,10 +1,11 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = "ndav"
3
- s.version = "0.0.3"
3
+ s.version = "0.0.5"
4
4
  s.summary = "N-Dimensional Array View"
5
5
  s.authors = ["Kitaiti Makoto"]
6
6
  s.licenses = ["BSD-2-Clause"]
7
- s.homepage = "https://gitlab.com/KitaitiMakoto/ndav"
7
+ s.homepage = "https://kitaitimakoto.gitlab.io/ndav"
8
+ s.metadata["source_code_uri"] = "https://gitlab.com/KitaitiMakoto/ndav"
8
9
 
9
10
  s.files = Dir.chdir(__dir__) {`git ls-files -z`.split("\x0")}
10
11
  s.extensions = ["ext/extconf.rb"]
@@ -20,4 +21,6 @@ Gem::Specification.new do |s|
20
21
  s.add_development_dependency "rubygems-requirements-system"
21
22
  s.add_development_dependency "red-arrow"
22
23
  s.add_development_dependency "ffi"
24
+ s.add_development_dependency "yard"
25
+ s.add_development_dependency "commonmarker"
23
26
  end
data/test/test_ffi.rb CHANGED
@@ -48,6 +48,14 @@ class TestFFI < TestBase
48
48
  assert_equal ndav.to_ptr.to_i, ptr.address
49
49
  end
50
50
 
51
+ def test_memory_view_from_ffi_memory_pointer
52
+ ptr = ::FFI::MemoryPointer.new(:int16, 3)
53
+ ptr.write_array_of_int16([1, 2, 3])
54
+ fmv = Fiddle::MemoryView.new(ptr)
55
+
56
+ assert_int_array fmv
57
+ end
58
+
51
59
  class TestConverter < self
52
60
  def test_ndav_from_ffi_memory_pointer
53
61
  ptr = ::FFI::MemoryPointer.new(:int16, 3)
data/test/test_ndav.rb CHANGED
@@ -28,4 +28,19 @@ class TestNDAV < TestBase
28
28
  assert_equal ptr, ndav.to_fiddle_pointer
29
29
  assert_int_array ndav
30
30
  end
31
+
32
+ def test_string
33
+ str = [1, 2, 3].pack("s*")
34
+ ndav = NDAV.from_string(str, format: "s")
35
+
36
+ assert_int_array ndav
37
+ end
38
+
39
+ def test_column_major
40
+ str = [1, 2, 3,
41
+ 4, 5, 6].pack("s*")
42
+ ndav = NDAV.from_string(str, format: "s", shape: [2, 3], strides: [2, 4])
43
+
44
+ assert_true ndav.column_major_contiguous?
45
+ end
31
46
  end
@@ -0,0 +1,79 @@
1
+ require_relative "helper"
2
+
3
+ class TestRactor < TestBase
4
+ ractor
5
+
6
+ def test_ractor
7
+ ractor = Ractor.new {
8
+ Ractor.receive
9
+ }
10
+ ndav = NDAV.new(Arrow::Int16Array.new([1, 2, 3]))
11
+ ractor << ndav
12
+ result = ractor.value
13
+
14
+ assert_not_same ndav, result
15
+ assert_int_array result
16
+ end
17
+
18
+ def test_ractor_from_string
19
+ ractor = Ractor.new {
20
+ Ractor.receive
21
+ }
22
+ ndav = NDAV.from_string([1, 2, 3].pack("s*"), format: "s")
23
+ ractor << ndav
24
+ result = ractor.value
25
+
26
+ assert_int_array result
27
+ end
28
+
29
+ def test_ractor_share
30
+ ractor = Ractor.new {
31
+ Ractor.receive
32
+ }
33
+ ndav = NDAV.new(Arrow::Int16Array.new([1, 2, 3]))
34
+ Ractor.make_shareable(ndav)
35
+ ractor << ndav
36
+ result = ractor.value
37
+
38
+ assert_same ndav, result
39
+ assert_int_array result
40
+ end
41
+
42
+ def test_port
43
+ ractor = Ractor.new {
44
+ ndav, port = Ractor.receive
45
+ port << ndav
46
+ }
47
+ ndav = NDAV.new(Arrow::Int16Array.new([1, 2, 3]))
48
+ port = Ractor::Port.new
49
+ ractor << [ndav, port]
50
+ result = port.receive
51
+
52
+ assert_not_same ndav, result
53
+ assert_int_array result
54
+ end
55
+
56
+ def test_port_share
57
+ ractor = Ractor.new {
58
+ ndav, port = Ractor.receive
59
+ port << ndav
60
+ }
61
+ ndav = NDAV.new(Arrow::Int16Array.new([1, 2, 3]))
62
+ Ractor.make_shareable(ndav)
63
+ port = Ractor::Port.new
64
+ ractor << [ndav, port]
65
+ result = port.receive
66
+
67
+ assert_same ndav, result
68
+ assert_int_array result
69
+ end
70
+
71
+ def test_ractor_not_initializable
72
+ ractor = Ractor.new {
73
+ NDAV.new(Arrow::Int16Array.new([1, 2, 3]))
74
+ }
75
+ assert_raise Ractor::RemoteError do
76
+ ractor.value
77
+ end
78
+ end
79
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ndav
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.0.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kitaiti Makoto
@@ -149,6 +149,34 @@ dependencies:
149
149
  - - ">="
150
150
  - !ruby/object:Gem::Version
151
151
  version: '0'
152
+ - !ruby/object:Gem::Dependency
153
+ name: yard
154
+ requirement: !ruby/object:Gem::Requirement
155
+ requirements:
156
+ - - ">="
157
+ - !ruby/object:Gem::Version
158
+ version: '0'
159
+ type: :development
160
+ prerelease: false
161
+ version_requirements: !ruby/object:Gem::Requirement
162
+ requirements:
163
+ - - ">="
164
+ - !ruby/object:Gem::Version
165
+ version: '0'
166
+ - !ruby/object:Gem::Dependency
167
+ name: commonmarker
168
+ requirement: !ruby/object:Gem::Requirement
169
+ requirements:
170
+ - - ">="
171
+ - !ruby/object:Gem::Version
172
+ version: '0'
173
+ type: :development
174
+ prerelease: false
175
+ version_requirements: !ruby/object:Gem::Requirement
176
+ requirements:
177
+ - - ">="
178
+ - !ruby/object:Gem::Version
179
+ version: '0'
152
180
  executables: []
153
181
  extensions:
154
182
  - ext/extconf.rb
@@ -156,16 +184,18 @@ extra_rdoc_files: []
156
184
  files:
157
185
  - ".gitignore"
158
186
  - ".gitlab-ci.yml"
187
+ - ".yardopts"
159
188
  - Gemfile
160
189
  - LICENSE.txt
161
190
  - README.md
162
191
  - Rakefile
163
192
  - ext/extconf.rb
164
193
  - ext/ndav.c
165
- - ext/ndav.h
166
194
  - lib/ndav.rb
167
195
  - lib/ndav/converter.rb
168
196
  - lib/ndav/ffi.rb
197
+ - lib/ndav/flags.rb
198
+ - lib/ndav/memory_viewable.rb
169
199
  - ndav.gemspec
170
200
  - test/helper.rb
171
201
  - test/test_converter.rb
@@ -173,10 +203,12 @@ files:
173
203
  - test/test_ffi.rb
174
204
  - test/test_ndav.rb
175
205
  - test/test_package.rb
176
- homepage: https://gitlab.com/KitaitiMakoto/ndav
206
+ - test/test_ractor.rb
207
+ homepage: https://kitaitimakoto.gitlab.io/ndav
177
208
  licenses:
178
209
  - BSD-2-Clause
179
- metadata: {}
210
+ metadata:
211
+ source_code_uri: https://gitlab.com/KitaitiMakoto/ndav
180
212
  rdoc_options: []
181
213
  require_paths:
182
214
  - lib
@@ -191,7 +223,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
191
223
  - !ruby/object:Gem::Version
192
224
  version: '0'
193
225
  requirements: []
194
- rubygems_version: 4.0.6
226
+ rubygems_version: 4.0.16
195
227
  specification_version: 4
196
228
  summary: N-Dimensional Array View
197
229
  test_files: []
data/ext/ndav.h DELETED
@@ -1,2 +0,0 @@
1
- #include <ruby.h>
2
- #include <ruby/memory_view.h>