expanse 0.4.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 (6) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +59 -0
  3. data/Rakefile +11 -0
  4. data/expanse.gemspec +14 -0
  5. data/lib/expanse.rb +415 -0
  6. metadata +47 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: ce88f8b1e4b7b624370805e71f07889013ed5fcc48df6a2d77a2866b617aef2a
4
+ data.tar.gz: 9e5315fbaa5ac09418678232ad510cec31b9c99193c83bc2582eef319cdd3ec1
5
+ SHA512:
6
+ metadata.gz: fa88cb5a5b3601e2239f8ca6b06d1d43318f4b9204ca9c33f859b4d583925110b96993e88ee1030fe65756010ea100782609abdfa990c74c944dd92f7f90aff0
7
+ data.tar.gz: 670528b144d51a7e254f58a175aa12b943fefd4bf88031e68bf1117d147d811bf780c0b94195392139229c1b85faace7bc35d7952b9a195a8e3b5c285e69b0f8
data/README.md ADDED
@@ -0,0 +1,59 @@
1
+ # Expanse Ruby Bindings (`expanse`)
2
+
3
+ High-performance, pure-Rust Judy arrays and digital trie engine for Ruby via FFI (`Fiddle`).
4
+
5
+ ## Installation
6
+
7
+ Add to your `Gemfile`:
8
+ ```ruby
9
+ gem "expanse"
10
+ ```
11
+
12
+ Or install directly:
13
+ ```bash
14
+ gem install expanse
15
+ ```
16
+
17
+ ## Quickstart
18
+
19
+ ```ruby
20
+ require "expanse"
21
+
22
+ # 1. Dynamic sparse 64-bit integer set (Judy1)
23
+ set = Expanse::Set.new
24
+ set.add(42)
25
+ set.add(100_000)
26
+ puts set.include?(42) # true
27
+ puts set.rank(100_000) # O(depth) rank
28
+ puts set.first # 42
29
+ puts set.last # 100000
30
+
31
+ # 2. Ordered 64-bit key-value map (JudyL)
32
+ map = Expanse::Map.new
33
+ map[42] = 1000
34
+ map[100] = 5000
35
+ puts map[42] # 1000
36
+ map.each do |k, v|
37
+ puts "Key #{k} -> #{v}"
38
+ end
39
+
40
+ # 3. String trie (JudySL)
41
+ strmap = Expanse::StrMap.new
42
+ strmap["/api/v1/users"] = 200
43
+ puts strmap["/api/v1/users"] # 200
44
+
45
+ # 4. Arbitrary binary bytes map (JudyHS)
46
+ bytesmap = Expanse::BytesMap.new
47
+ bytesmap["binary\x00key".b] = 9999
48
+ puts bytesmap["binary\x00key".b] # 9999
49
+
50
+ # 5. Large-value off-heap blob map
51
+ blobmap = Expanse::BlobMap.new
52
+ blobmap.set(100, "hello world", hot_meta: 1234)
53
+ val, meta = blobmap.get(100)
54
+ puts "#{val} (meta: #{meta})"
55
+ ```
56
+
57
+ ## Documentation
58
+
59
+ For complete API specifications and benchmarks, see [docs/bindings/ruby.md](https://github.com/orieg/expanse/blob/main/docs/bindings/ruby.md).
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rake/testtask"
4
+
5
+ Rake::TestTask.new(:test) do |t|
6
+ t.libs << "lib"
7
+ t.test_files = FileList["test/**/test_*.rb"]
8
+ t.verbose = true
9
+ end
10
+
11
+ task default: :test
data/expanse.gemspec ADDED
@@ -0,0 +1,14 @@
1
+ Gem::Specification.new do |spec|
2
+ spec.name = "expanse"
3
+ spec.version = "0.4.1"
4
+ spec.authors = ["Nicolas Brousse"]
5
+ spec.email = ["nicolas@brousse.info"]
6
+ spec.summary = "Expanse: clean-room, pure-Rust Judy arrays"
7
+ spec.description = "Ruby bindings for the expanse library."
8
+ spec.homepage = "https://github.com/orieg/expanse"
9
+ spec.license = "MIT OR Apache-2.0"
10
+
11
+ spec.files = Dir["lib/**/*.rb", "README.md", "expanse.gemspec", "Rakefile"]
12
+ spec.require_paths = ["lib"]
13
+ spec.required_ruby_version = ">= 3.0.0"
14
+ end
data/lib/expanse.rb ADDED
@@ -0,0 +1,415 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fiddle"
4
+ require "fiddle/import"
5
+
6
+ module Expanse
7
+ VERSION = "0.4.1"
8
+
9
+ module Native
10
+ extend Fiddle::Importer
11
+
12
+ lib_paths = [
13
+ File.expand_path("../../../target/release/libexpanse.dylib", __dir__),
14
+ File.expand_path("../../../target/release/libexpanse.so", __dir__),
15
+ File.expand_path("../../../target/release/expanse.dll", __dir__),
16
+ File.expand_path("../../../target/debug/libexpanse.dylib", __dir__),
17
+ File.expand_path("../../../target/debug/libexpanse.so", __dir__),
18
+ "libexpanse.dylib",
19
+ "libexpanse.so",
20
+ "expanse.dll"
21
+ ]
22
+
23
+ loaded = false
24
+ lib_paths.each do |path|
25
+ if File.exist?(path) || !path.include?("/")
26
+ begin
27
+ dlload path
28
+ loaded = true
29
+ break
30
+ rescue StandardError
31
+ # continue searching
32
+ end
33
+ end
34
+ end
35
+
36
+ raise "Could not load libexpanse native library" unless loaded
37
+
38
+ # Library metadata
39
+ extern "const char* expanse_version(void)"
40
+
41
+ # Set
42
+ extern "void* expanse_set_new(void)"
43
+ extern "void expanse_set_free(void*)"
44
+ extern "int expanse_set_insert(void*, unsigned long long)"
45
+ extern "int expanse_set_remove(void*, unsigned long long)"
46
+ extern "int expanse_set_contains(void*, unsigned long long)"
47
+ extern "unsigned long long expanse_set_len(void*)"
48
+ extern "size_t expanse_set_mem_used(void*)"
49
+ extern "void expanse_set_clear(void*)"
50
+ extern "int expanse_set_first(void*, void*)"
51
+ extern "int expanse_set_last(void*, void*)"
52
+ extern "int expanse_set_next_after(void*, unsigned long long, void*)"
53
+ extern "int expanse_set_prev_before(void*, unsigned long long, void*)"
54
+ extern "unsigned long long expanse_set_count_below(void*, unsigned long long)"
55
+ extern "unsigned long long expanse_set_count_range(void*, unsigned long long, unsigned long long)"
56
+ extern "int expanse_set_by_count(void*, unsigned long long, void*)"
57
+
58
+ # Map
59
+ extern "void* expanse_map_new(void)"
60
+ extern "void expanse_map_free(void*)"
61
+ extern "int expanse_map_insert(void*, unsigned long long, unsigned long long, void*)"
62
+ extern "int expanse_map_get(void*, unsigned long long, void*)"
63
+ extern "int expanse_map_remove(void*, unsigned long long, void*)"
64
+ extern "unsigned long long expanse_map_len(void*)"
65
+ extern "size_t expanse_map_mem_used(void*)"
66
+ extern "void expanse_map_clear(void*)"
67
+ extern "int expanse_map_first(void*, void*, void*)"
68
+ extern "int expanse_map_last(void*, void*, void*)"
69
+ extern "int expanse_map_next_after(void*, unsigned long long, void*, void*)"
70
+ extern "int expanse_map_prev_before(void*, unsigned long long, void*, void*)"
71
+
72
+ # StrMap
73
+ extern "void* expanse_strmap_new(void)"
74
+ extern "void expanse_strmap_free(void*)"
75
+ extern "int expanse_strmap_insert(void*, const char*, unsigned long long, void*)"
76
+ extern "int expanse_strmap_get(void*, const char*, void*)"
77
+ extern "int expanse_strmap_remove(void*, const char*, void*)"
78
+ extern "unsigned long long expanse_strmap_len(void*)"
79
+ extern "void expanse_strmap_clear(void*)"
80
+
81
+ # BytesMap
82
+ extern "void* expanse_bytesmap_new(void)"
83
+ extern "void expanse_bytesmap_free(void*)"
84
+ extern "int expanse_bytesmap_insert(void*, const void*, size_t, unsigned long long, void*)"
85
+ extern "int expanse_bytesmap_get(void*, const void*, size_t, void*)"
86
+ extern "int expanse_bytesmap_remove(void*, const void*, size_t, void*)"
87
+ extern "unsigned long long expanse_bytesmap_len(void*)"
88
+ extern "void expanse_bytesmap_clear(void*)"
89
+
90
+ # BlobMap
91
+ extern "void* expanse_blob_map_new(size_t)"
92
+ extern "void expanse_blob_map_free(void*)"
93
+ extern "int expanse_blob_map_insert(void*, unsigned long long, const void*, size_t, unsigned int)"
94
+ extern "int expanse_blob_map_get(void*, unsigned long long, void*)"
95
+ extern "int expanse_blob_map_remove(void*, unsigned long long)"
96
+ extern "unsigned long long expanse_blob_map_len(void*)"
97
+ extern "void expanse_blob_map_clear(void*)"
98
+ extern "int expanse_blob_map_contains_key(void*, unsigned long long)"
99
+ end
100
+
101
+ def self.version
102
+ Native.expanse_version.to_s
103
+ end
104
+
105
+ class Set
106
+ include Enumerable
107
+
108
+ def initialize
109
+ @ptr = Native.expanse_set_new
110
+ ObjectSpace.define_finalizer(self, self.class.finalize(@ptr))
111
+ end
112
+
113
+ def self.finalize(ptr)
114
+ proc { Native.expanse_set_free(ptr) if ptr && !ptr.null? }
115
+ end
116
+
117
+ def add(key)
118
+ Native.expanse_set_insert(@ptr, key) != 0
119
+ end
120
+ alias << add
121
+
122
+ def delete(key)
123
+ Native.expanse_set_remove(@ptr, key) != 0
124
+ end
125
+ alias remove delete
126
+
127
+ def include?(key)
128
+ Native.expanse_set_contains(@ptr, key) != 0
129
+ end
130
+ alias key? include?
131
+ alias member? include?
132
+
133
+ def size
134
+ Native.expanse_set_len(@ptr)
135
+ end
136
+ alias length size
137
+ alias count size
138
+
139
+ def empty?
140
+ size.zero?
141
+ end
142
+
143
+ def clear
144
+ Native.expanse_set_clear(@ptr)
145
+ self
146
+ end
147
+
148
+ def first
149
+ buf = Fiddle::Pointer.malloc(8)
150
+ if Native.expanse_set_first(@ptr, buf) != 0
151
+ buf.to_str(8).unpack1("Q<")
152
+ end
153
+ end
154
+
155
+ def last
156
+ buf = Fiddle::Pointer.malloc(8)
157
+ if Native.expanse_set_last(@ptr, buf) != 0
158
+ buf.to_str(8).unpack1("Q<")
159
+ end
160
+ end
161
+
162
+ def next(key)
163
+ buf = Fiddle::Pointer.malloc(8)
164
+ if Native.expanse_set_next_after(@ptr, key, buf) != 0
165
+ buf.to_str(8).unpack1("Q<")
166
+ end
167
+ end
168
+
169
+ def prev(key)
170
+ buf = Fiddle::Pointer.malloc(8)
171
+ if Native.expanse_set_prev_before(@ptr, key, buf) != 0
172
+ buf.to_str(8).unpack1("Q<")
173
+ end
174
+ end
175
+
176
+ def rank(key)
177
+ Native.expanse_set_count_below(@ptr, key)
178
+ end
179
+
180
+ def select(k)
181
+ buf = Fiddle::Pointer.malloc(8)
182
+ if Native.expanse_set_by_count(@ptr, k, buf) != 0
183
+ buf.to_str(8).unpack1("Q<")
184
+ end
185
+ end
186
+
187
+ def count_range(lo, hi)
188
+ Native.expanse_set_count_range(@ptr, lo, hi)
189
+ end
190
+
191
+ def each
192
+ return enum_for(:each) unless block_given?
193
+ cur = first
194
+ while cur
195
+ yield cur
196
+ cur = self.next(cur)
197
+ end
198
+ end
199
+ end
200
+
201
+ class Map
202
+ include Enumerable
203
+
204
+ def initialize
205
+ @ptr = Native.expanse_map_new
206
+ ObjectSpace.define_finalizer(self, self.class.finalize(@ptr))
207
+ end
208
+
209
+ def self.finalize(ptr)
210
+ proc { Native.expanse_map_free(ptr) if ptr && !ptr.null? }
211
+ end
212
+
213
+ def []=(key, val)
214
+ Native.expanse_map_insert(@ptr, key, val, nil)
215
+ val
216
+ end
217
+ alias set []=
218
+
219
+ def [](key)
220
+ buf = Fiddle::Pointer.malloc(8)
221
+ if Native.expanse_map_get(@ptr, key, buf) != 0
222
+ buf.to_str(8).unpack1("Q<")
223
+ end
224
+ end
225
+ alias get []
226
+
227
+ def delete(key)
228
+ buf = Fiddle::Pointer.malloc(8)
229
+ if Native.expanse_map_remove(@ptr, key, buf) != 0
230
+ buf.to_str(8).unpack1("Q<")
231
+ end
232
+ end
233
+
234
+ def key?(key)
235
+ !self[key].nil?
236
+ end
237
+ alias include? key?
238
+
239
+ def size
240
+ Native.expanse_map_len(@ptr)
241
+ end
242
+ alias length size
243
+
244
+ def clear
245
+ Native.expanse_map_clear(@ptr)
246
+ self
247
+ end
248
+
249
+ def first
250
+ k_buf = Fiddle::Pointer.malloc(8)
251
+ v_buf = Fiddle::Pointer.malloc(8)
252
+ if Native.expanse_map_first(@ptr, k_buf, v_buf) != 0
253
+ [k_buf.to_str(8).unpack1("Q<"), v_buf.to_str(8).unpack1("Q<")]
254
+ end
255
+ end
256
+
257
+ def next(key)
258
+ k_buf = Fiddle::Pointer.malloc(8)
259
+ v_buf = Fiddle::Pointer.malloc(8)
260
+ if Native.expanse_map_next_after(@ptr, key, k_buf, v_buf) != 0
261
+ [k_buf.to_str(8).unpack1("Q<"), v_buf.to_str(8).unpack1("Q<")]
262
+ end
263
+ end
264
+
265
+ def each
266
+ return enum_for(:each) unless block_given?
267
+ pair = first
268
+ while pair
269
+ yield pair[0], pair[1]
270
+ pair = self.next(pair[0])
271
+ end
272
+ end
273
+ end
274
+
275
+ class StrMap
276
+ def initialize
277
+ @ptr = Native.expanse_strmap_new
278
+ ObjectSpace.define_finalizer(self, self.class.finalize(@ptr))
279
+ end
280
+
281
+ def self.finalize(ptr)
282
+ proc { Native.expanse_strmap_free(ptr) if ptr && !ptr.null? }
283
+ end
284
+
285
+ def []=(key, val)
286
+ s = key.to_s
287
+ Native.expanse_strmap_insert(@ptr, s, val, nil)
288
+ val
289
+ end
290
+ alias set []=
291
+
292
+ def [](key)
293
+ s = key.to_s
294
+ buf = Fiddle::Pointer.malloc(8)
295
+ if Native.expanse_strmap_get(@ptr, s, buf) != 0
296
+ buf.to_str(8).unpack1("Q<")
297
+ end
298
+ end
299
+ alias get []
300
+
301
+ def delete(key)
302
+ s = key.to_s
303
+ buf = Fiddle::Pointer.malloc(8)
304
+ if Native.expanse_strmap_remove(@ptr, s, buf) != 0
305
+ buf.to_str(8).unpack1("Q<")
306
+ end
307
+ end
308
+
309
+ def key?(key)
310
+ !self[key].nil?
311
+ end
312
+
313
+ def size
314
+ Native.expanse_strmap_len(@ptr)
315
+ end
316
+
317
+ def clear
318
+ Native.expanse_strmap_clear(@ptr)
319
+ self
320
+ end
321
+ end
322
+
323
+ class BytesMap
324
+ def initialize
325
+ @ptr = Native.expanse_bytesmap_new
326
+ ObjectSpace.define_finalizer(self, self.class.finalize(@ptr))
327
+ end
328
+
329
+ def self.finalize(ptr)
330
+ proc { Native.expanse_bytesmap_free(ptr) if ptr && !ptr.null? }
331
+ end
332
+
333
+ def []=(key, val)
334
+ b = key.b
335
+ Native.expanse_bytesmap_insert(@ptr, b, b.bytesize, val, nil)
336
+ val
337
+ end
338
+ alias set []=
339
+
340
+ def [](key)
341
+ b = key.b
342
+ buf = Fiddle::Pointer.malloc(8)
343
+ if Native.expanse_bytesmap_get(@ptr, b, b.bytesize, buf) != 0
344
+ buf.to_str(8).unpack1("Q<")
345
+ end
346
+ end
347
+ alias get []
348
+
349
+ def delete(key)
350
+ b = key.b
351
+ buf = Fiddle::Pointer.malloc(8)
352
+ if Native.expanse_bytesmap_remove(@ptr, b, b.bytesize, buf) != 0
353
+ buf.to_str(8).unpack1("Q<")
354
+ end
355
+ end
356
+
357
+ def key?(key)
358
+ !self[key].nil?
359
+ end
360
+
361
+ def size
362
+ Native.expanse_bytesmap_len(@ptr)
363
+ end
364
+
365
+ def clear
366
+ Native.expanse_bytesmap_clear(@ptr)
367
+ self
368
+ end
369
+ end
370
+
371
+ class BlobMap
372
+ def initialize(chunk_size: 0)
373
+ @ptr = Native.expanse_blob_map_new(chunk_size)
374
+ ObjectSpace.define_finalizer(self, self.class.finalize(@ptr))
375
+ end
376
+
377
+ def self.finalize(ptr)
378
+ proc { Native.expanse_blob_map_free(ptr) if ptr && !ptr.null? }
379
+ end
380
+
381
+ def set(key, payload, hot_meta: 0)
382
+ b = payload.b
383
+ Native.expanse_blob_map_insert(@ptr, key, b, b.bytesize, hot_meta) != 0
384
+ end
385
+
386
+ def get(key)
387
+ # ExpanseBlobView: ptr (8B), len (8B), hot_meta (4B), is_inline (1B) + padding (3B) = 24 bytes
388
+ view_buf = Fiddle::Pointer.malloc(24)
389
+ if Native.expanse_blob_map_get(@ptr, key, view_buf) != 0
390
+ raw_ptr = view_buf[0, 8].unpack1("Q<")
391
+ len = view_buf[8, 8].unpack1("Q<")
392
+ meta = view_buf[16, 4].unpack1("L<")
393
+ val = len.zero? ? "" : Fiddle::Pointer.new(raw_ptr).to_str(len)
394
+ [val, meta]
395
+ end
396
+ end
397
+
398
+ def delete(key)
399
+ Native.expanse_blob_map_remove(@ptr, key) != 0
400
+ end
401
+
402
+ def key?(key)
403
+ Native.expanse_blob_map_contains_key(@ptr, key) != 0
404
+ end
405
+
406
+ def size
407
+ Native.expanse_blob_map_len(@ptr)
408
+ end
409
+
410
+ def clear
411
+ Native.expanse_blob_map_clear(@ptr)
412
+ self
413
+ end
414
+ end
415
+ end
metadata ADDED
@@ -0,0 +1,47 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: expanse
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.4.1
5
+ platform: ruby
6
+ authors:
7
+ - Nicolas Brousse
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-08-26 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Ruby bindings for the expanse library.
14
+ email:
15
+ - nicolas@brousse.info
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - README.md
21
+ - Rakefile
22
+ - expanse.gemspec
23
+ - lib/expanse.rb
24
+ homepage: https://github.com/orieg/expanse
25
+ licenses:
26
+ - MIT OR Apache-2.0
27
+ metadata: {}
28
+ post_install_message:
29
+ rdoc_options: []
30
+ require_paths:
31
+ - lib
32
+ required_ruby_version: !ruby/object:Gem::Requirement
33
+ requirements:
34
+ - - ">="
35
+ - !ruby/object:Gem::Version
36
+ version: 3.0.0
37
+ required_rubygems_version: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: '0'
42
+ requirements: []
43
+ rubygems_version: 3.5.22
44
+ signing_key:
45
+ specification_version: 4
46
+ summary: 'Expanse: clean-room, pure-Rust Judy arrays'
47
+ test_files: []