tinybits 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8c60cd0920984c872762a71f597b65c2c9712791ea0b3e8a54a20282536f8a6e
4
- data.tar.gz: 40d72eea34b0e6c07f6747955edca46dfed52a9a624f8e19d2e93ae8a7f6dfa6
3
+ metadata.gz: d36aff126ea60de67bc4050896a329f39e53a155cf50f4b12f35602757dc4ab1
4
+ data.tar.gz: 610909057ad8ae5f8fa0dab0e1dd5ee03105b7fb0036b11e3ac22eea97522cee
5
5
  SHA512:
6
- metadata.gz: 1a4086aa883b8af346ff003b3b414aea443fd56caff4f1da6730cf974365886ae78eceb84c1d37d9d4aea026121cafc0eb5febb4ee3c1ff03e48dc0b646fb203
7
- data.tar.gz: 063a02e5715833da64c75fd54e6a353e3d6cf89b61d704085edd3791a6b31131398174b3d27e5e1189fd1e0137644aed2358a26e0b2576b717f8775b5a951c0b
6
+ metadata.gz: fb4ce2802d01a5a62a119edc8f8c7ea06aff12edf0eff1d9b0d3a758d096e36c40c49c3ca52c22368da199b65b75900db1a5296580e38b9d2c00da62b80d6535
7
+ data.tar.gz: 16355ff8ae048bf3ae8d50eb2067cc42e01e21da1a396713d3a74e831e163658f3631dc19c09fc52c867fcf4b24bd29399029f44fabe9ac92be99eaccaa66a9b
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * TinyBits Amalgamated Header
3
- * Generated on: Fri 30 Oct 2026 12:29:55 PM CET
3
+ * Generated on: Tue 03 Nov 2026 03:43:24 AM CET
4
4
  */
5
5
 
6
6
  #ifndef TINY_BITS_H
@@ -15,7 +15,8 @@
15
15
  #include <math.h>
16
16
  #include <stdio.h>
17
17
 
18
- #define TB_HASH_SIZE 128
18
+ #define TB_HASH_BITS 7
19
+ #define TB_HASH_SIZE (1 << TB_HASH_BITS)
19
20
  #define TB_HASH_CACHE_SIZE 256
20
21
  #define MAX_BYTES 9
21
22
  #define TB_DDP_STR_LEN_MAX 128
@@ -84,7 +85,7 @@ typedef struct HashTable {
84
85
  uint32_t next_id;
85
86
  uint32_t cache_size;
86
87
  uint32_t cache_pos;
87
- uint8_t bins[TB_HASH_SIZE];
88
+ uint16_t bins[TB_HASH_SIZE]; // 1-based cache index, must hold TB_HASH_CACHE_SIZE
88
89
  } HashTable;
89
90
 
90
91
  static inline uint32_t fast_hash_32(const char* str, uint16_t len) {
@@ -97,6 +98,11 @@ static inline uint32_t fast_hash_32(const char* str, uint16_t len) {
97
98
  return hash;
98
99
  }
99
100
 
101
+ // bin index from the top bits of a multiplicative mix (the low bits alone are just the last byte)
102
+ static inline uint32_t tb_hash_bin(uint32_t hash_code) {
103
+ return (hash_code * 0x9E3779B1u) >> (32 - TB_HASH_BITS);
104
+ }
105
+
100
106
  static inline int encode_varint(uint64_t value, uint8_t* buffer) {
101
107
  if (value <= 240) {
102
108
  buffer[0] = (uint8_t)value; // 1 byte
@@ -413,7 +419,9 @@ typedef struct tiny_bits_packer {
413
419
  size_t capacity; // Total allocated size of the buffer
414
420
  size_t current_pos; // Current position in the buffer (write position)
415
421
  HashTable encode_table; // Add the hash table here
416
- HashTable dictionary;
422
+ size_t dict_len; // dictionary bytes stored at the start of buffer
423
+ uint32_t dict_count; // dedupe cache entries taken by the dictionary
424
+ uint16_t dict_bins[TB_HASH_SIZE]; // encode_table.bins right after loading the dictionary
417
425
  uint8_t features;
418
426
  // Add any other encoder-specific state here if needed (e.g., string deduplication table later)
419
427
  } tiny_bits_packer;
@@ -451,6 +459,9 @@ tiny_bits_packer *tiny_bits_packer_create(size_t initial_capacity, uint8_t featu
451
459
  encoder->capacity = initial_capacity;
452
460
  encoder->current_pos = 0;
453
461
  encoder->features = features;
462
+ encoder->dict_len = 0;
463
+ encoder->dict_count = 0;
464
+ memset(encoder->dict_bins, 0, sizeof(encoder->dict_bins));
454
465
 
455
466
  // Only allocate hash table if deduplication is enabled
456
467
  if (features & TB_FEATURE_STRING_DEDUPE) {
@@ -464,7 +475,7 @@ tiny_bits_packer *tiny_bits_packer_create(size_t initial_capacity, uint8_t featu
464
475
  encoder->encode_table.cache_size = TB_HASH_CACHE_SIZE;
465
476
  encoder->encode_table.cache_pos = 0;
466
477
  encoder->encode_table.next_id = 0;
467
- memset(encoder->encode_table.bins, 0, TB_HASH_SIZE * sizeof(uint8_t));
478
+ memset(encoder->encode_table.bins, 0, sizeof(encoder->encode_table.bins));
468
479
  } else {
469
480
  encoder->encode_table.cache = NULL;
470
481
  encoder->encode_table.cache_size = 0;
@@ -482,15 +493,82 @@ tiny_bits_packer *tiny_bits_packer_create(size_t initial_capacity, uint8_t featu
482
493
  *
483
494
  * @note This function allows for more efficient packing by reusing the same packer object
484
495
  */
485
- inline void tiny_bits_packer_reset(tiny_bits_packer *encoder) {
496
+ static inline void tiny_bits_packer_reset(tiny_bits_packer *encoder) {
486
497
  if (!encoder) return;
487
- encoder->current_pos = 0;
498
+ encoder->current_pos = encoder->dict_len;
488
499
  if (encoder->features & TB_FEATURE_STRING_DEDUPE) {
489
500
  encoder->encode_table.next_id = 0;
490
- encoder->encode_table.cache_pos = 0;
491
- memset(encoder->encode_table.bins, 0, TB_HASH_SIZE * sizeof(uint8_t));
501
+ encoder->encode_table.cache_pos = encoder->dict_count;
502
+ memcpy(encoder->encode_table.bins, encoder->dict_bins, sizeof(encoder->dict_bins));
492
503
  }
493
-
504
+ }
505
+
506
+ /**
507
+ * @brief Loads an external dictionary of strings used for deduplication
508
+ *
509
+ * @param encoder The packer instance (must have TB_FEATURE_STRING_DEDUPE)
510
+ * @param strs Array of string pointers (order is significant)
511
+ * @param lens Array of string lengths
512
+ * @param count Number of strings
513
+ * @return 1 on success, 0 on error
514
+ *
515
+ * @note The dictionary is not written to the output, the unpacker must be given the same dictionary.
516
+ * Strings are copied, and the packer is reset. Strings are assigned ids exactly as if they were packed
517
+ * (in order) at the start of every output, so only the first 256 dedupable strings (2-128 bytes) count.
518
+ */
519
+ static inline int tiny_bits_packer_set_dict(tiny_bits_packer *encoder, const char **strs, const size_t *lens, size_t count) {
520
+ if (!encoder || !(encoder->features & TB_FEATURE_STRING_DEDUPE)) return 0;
521
+ HashTable *table = &encoder->encode_table;
522
+ encoder->dict_len = 0;
523
+ encoder->dict_count = 0;
524
+ memset(encoder->dict_bins, 0, sizeof(encoder->dict_bins));
525
+ tiny_bits_packer_reset(encoder);
526
+
527
+ for (size_t i = 0; i < count && table->cache_pos < TB_HASH_CACHE_SIZE; i++) {
528
+ const char *str = strs[i];
529
+ size_t len = lens[i];
530
+ if (len < 2 || len > 128) continue;
531
+ uint32_t hash_code = fast_hash_32(str, len);
532
+ uint32_t hash = tb_hash_bin(hash_code);
533
+ uint16_t index = table->bins[hash];
534
+ while (index > 0) {
535
+ HashEntry entry = table->cache[index - 1];
536
+ if (hash_code == entry.hash && len == entry.length
537
+ && memcmp(str, encoder->buffer + entry.offset, len) == 0) break;
538
+ index = entry.next_index;
539
+ }
540
+ if (index > 0) continue; // duplicate, packing it would emit a ref and take no id
541
+
542
+ uint8_t *buffer = tiny_bits_packer_ensure_capacity(encoder, len);
543
+ if (!buffer) return 0;
544
+ memcpy(buffer, str, len);
545
+ HashEntry *new_entry = &table->cache[table->cache_pos++];
546
+ new_entry->hash = hash_code;
547
+ new_entry->length = len;
548
+ new_entry->offset = encoder->current_pos;
549
+ new_entry->next_index = table->bins[hash];
550
+ table->bins[hash] = table->cache_pos;
551
+ encoder->current_pos += len;
552
+ }
553
+
554
+ encoder->dict_len = encoder->current_pos;
555
+ encoder->dict_count = table->cache_pos;
556
+ memcpy(encoder->dict_bins, table->bins, sizeof(encoder->dict_bins));
557
+ return 1;
558
+ }
559
+
560
+ /**
561
+ * @brief Returns a pointer to the packed output (excludes the dictionary)
562
+ */
563
+ static inline const unsigned char *tiny_bits_packer_data(const tiny_bits_packer *encoder) {
564
+ return encoder->buffer + encoder->dict_len;
565
+ }
566
+
567
+ /**
568
+ * @brief Returns the size of the packed output (excludes the dictionary)
569
+ */
570
+ static inline size_t tiny_bits_packer_size(const tiny_bits_packer *encoder) {
571
+ return encoder->current_pos - encoder->dict_len;
494
572
  }
495
573
 
496
574
  /**
@@ -715,8 +793,8 @@ static inline int pack_str(tiny_bits_packer *encoder, char* str, uint32_t str_le
715
793
  uint32_t hash = 0;
716
794
  if ((encoder->features & TB_FEATURE_STRING_DEDUPE) && str_len >= 2 && str_len <= 128) {
717
795
  hash_code = fast_hash_32(str, str_len);
718
- hash = hash_code % TB_HASH_SIZE;
719
- uint8_t index = encoder->encode_table.bins[hash];
796
+ hash = tb_hash_bin(hash_code);
797
+ uint16_t index = encoder->encode_table.bins[hash];
720
798
  while (index > 0) {
721
799
  HashEntry entry = encoder->encode_table.cache[index - 1];
722
800
  if (hash_code == entry.hash
@@ -940,7 +1018,8 @@ typedef struct tiny_bits_unpacker {
940
1018
  } *strings; // Array of decoded strings
941
1019
  size_t strings_size; // Capacity of strings array
942
1020
  size_t strings_count; // Number of strings stored
943
- HashTable dictionary;
1021
+ char *dict; // copy of the dictionary bytes, strings[0..dict_count) point into it
1022
+ size_t dict_count; // number of strings entries taken by the dictionary
944
1023
  } tiny_bits_unpacker;
945
1024
 
946
1025
  /**
@@ -962,9 +1041,63 @@ tiny_bits_unpacker *tiny_bits_unpacker_create(void) {
962
1041
  return NULL;
963
1042
  }
964
1043
  decoder->strings_count = 0;
1044
+ decoder->dict = NULL;
1045
+ decoder->dict_count = 0;
965
1046
  return decoder;
966
1047
  }
967
1048
 
1049
+ /**
1050
+ * @brief Loads an external dictionary of strings, must match the one given to the packer
1051
+ *
1052
+ * @param decoder The unpacker instance
1053
+ * @param strs Array of string pointers (order is significant)
1054
+ * @param lens Array of string lengths
1055
+ * @param count Number of strings
1056
+ * @return 1 on success, 0 on error
1057
+ *
1058
+ * @note Strings are copied. Dictionary strings are returned as dedupe references, with ids 1..n
1059
+ * (see unpack_value), so client code can map them to pre-built objects.
1060
+ */
1061
+ static inline int tiny_bits_unpacker_set_dict(tiny_bits_unpacker *decoder, const char **strs, const size_t *lens, size_t count) {
1062
+ if (!decoder) return 0;
1063
+ size_t total = 0;
1064
+ for (size_t i = 0; i < count; i++) total += lens[i];
1065
+ char *dict = (char *)malloc(total ? total : 1);
1066
+ if (!dict) return 0;
1067
+ free(decoder->dict);
1068
+ decoder->dict = dict;
1069
+ decoder->dict_count = 0;
1070
+ decoder->strings_count = 0;
1071
+
1072
+ size_t offset = 0;
1073
+ for (size_t i = 0; i < count && decoder->dict_count < TB_HASH_CACHE_SIZE; i++) {
1074
+ size_t len = lens[i];
1075
+ if (len < 2 || len > 128) continue;
1076
+ int duplicate = 0;
1077
+ for (size_t j = 0; j < decoder->dict_count; j++) {
1078
+ if (decoder->strings[j].length == len && memcmp(decoder->strings[j].str, strs[i], len) == 0) {
1079
+ duplicate = 1;
1080
+ break;
1081
+ }
1082
+ }
1083
+ if (duplicate) continue;
1084
+ if (decoder->dict_count >= decoder->strings_size) {
1085
+ size_t new_size = decoder->strings_size * 2;
1086
+ void *new_strings = realloc(decoder->strings, new_size * sizeof(*decoder->strings));
1087
+ if (!new_strings) return 0;
1088
+ decoder->strings = new_strings;
1089
+ decoder->strings_size = new_size;
1090
+ }
1091
+ memcpy(dict + offset, strs[i], len);
1092
+ decoder->strings[decoder->dict_count].str = dict + offset;
1093
+ decoder->strings[decoder->dict_count].length = len;
1094
+ decoder->dict_count++;
1095
+ offset += len;
1096
+ }
1097
+ decoder->strings_count = decoder->dict_count;
1098
+ return 1;
1099
+ }
1100
+
968
1101
  /**
969
1102
  * @breif Provides a buffer to the unpacker for unpacking
970
1103
  *
@@ -982,7 +1115,7 @@ static inline void tiny_bits_unpacker_set_buffer(tiny_bits_unpacker *decoder, co
982
1115
  decoder->buffer = buffer;
983
1116
  decoder->size = size;
984
1117
  decoder->current_pos = 0;
985
- decoder->strings_count = 0;
1118
+ decoder->strings_count = decoder->dict_count;
986
1119
  }
987
1120
 
988
1121
  /**
@@ -995,7 +1128,7 @@ static inline void tiny_bits_unpacker_set_buffer(tiny_bits_unpacker *decoder, co
995
1128
  static inline void tiny_bits_unpacker_reset(tiny_bits_unpacker *decoder) {
996
1129
  if (!decoder) return;
997
1130
  decoder->current_pos = 0;
998
- decoder->strings_count = 0;
1131
+ decoder->strings_count = decoder->dict_count;
999
1132
  }
1000
1133
 
1001
1134
 
@@ -1010,6 +1143,7 @@ void tiny_bits_unpacker_destroy(tiny_bits_unpacker *decoder) {
1010
1143
  if (decoder->strings) {
1011
1144
  free(decoder->strings);
1012
1145
  }
1146
+ free(decoder->dict);
1013
1147
  free(decoder);
1014
1148
  }
1015
1149
 
@@ -1215,39 +1349,31 @@ static inline enum tiny_bits_type unpack_value(tiny_bits_unpacker *decoder, tiny
1215
1349
  }
1216
1350
 
1217
1351
  uint8_t tag = decoder->buffer[decoder->current_pos++];
1218
- // Dispatch based on tag
1219
- if ((tag & TB_INT_TAG) == TB_INT_TAG) { // Integers
1352
+ // Dispatch on tag ranges, each tag group starts at a power of two
1353
+ if (tag >= TB_INT_TAG) { // Integers
1220
1354
  return _unpack_int(decoder, tag, value);
1221
- } else if ((tag & TB_STR_TAG) == TB_STR_TAG) { // Strings
1355
+ } else if (tag >= TB_STR_TAG) { // Strings
1222
1356
  return _unpack_str(decoder, tag, value);
1223
- } else if (tag == TB_NIL_TAG) {
1224
- return TINY_BITS_NULL;
1225
- } else if (tag == TB_NAN_TAG) {
1226
- return TINY_BITS_NAN;
1227
- } else if (tag == TB_INF_TAG) {
1228
- return TINY_BITS_INF;
1229
- } else if (tag == TB_NNF_TAG) {
1230
- return TINY_BITS_N_INF;
1231
- } else if ((tag & TB_DBL_TAG) == TB_DBL_TAG) { // Doubles
1357
+ } else if (tag >= TB_DBL_TAG) { // Doubles
1358
+ if (tag == TB_NAN_TAG) return TINY_BITS_NAN;
1359
+ if (tag == TB_INF_TAG) return TINY_BITS_INF;
1360
+ if (tag == TB_NNF_TAG) return TINY_BITS_N_INF;
1232
1361
  return _unpack_double(decoder, tag, value);
1233
- } else if ((tag & TB_MAP_TAG) == TB_MAP_TAG) { // Maps
1362
+ } else if (tag >= TB_MAP_TAG) { // Maps
1234
1363
  return _unpack_map(decoder, tag, value);
1235
- } else if ((tag & TB_ARR_TAG) == TB_ARR_TAG) { // Arrays
1364
+ } else if (tag >= TB_ARR_TAG) { // Arrays
1236
1365
  return _unpack_arr(decoder, tag, value);
1237
- } else if (tag == TB_BLB_TAG) { // Blob
1238
- return _unpack_blob(decoder, tag, value);
1239
- } else if (tag == TB_DTM_TAG) {
1240
- return _unpack_datetime(decoder, tag, value);
1241
- } else if (tag == TB_SEP_TAG) {
1242
- return TINY_BITS_SEP;
1243
- } else if (tag == TB_EXT_TAG) {
1244
- return TINY_BITS_EXT;
1245
- } else if (tag == TB_TRU_TAG) {
1246
- return TINY_BITS_TRUE;
1247
- } else if (tag == TB_FLS_TAG) {
1248
- return TINY_BITS_FALSE;
1249
1366
  }
1250
- return TINY_BITS_ERROR; // Unknown tag
1367
+ switch (tag) {
1368
+ case TB_NIL_TAG: return TINY_BITS_NULL;
1369
+ case TB_TRU_TAG: return TINY_BITS_TRUE;
1370
+ case TB_FLS_TAG: return TINY_BITS_FALSE;
1371
+ case TB_BLB_TAG: return _unpack_blob(decoder, tag, value);
1372
+ case TB_DTM_TAG: return _unpack_datetime(decoder, tag, value);
1373
+ case TB_SEP_TAG: return TINY_BITS_SEP;
1374
+ case TB_EXT_TAG: return TINY_BITS_EXT;
1375
+ }
1376
+ return TINY_BITS_ERROR; // Unknown tag (TB_NXT_TAG)
1251
1377
  }
1252
1378
 
1253
1379
 
@@ -25,7 +25,7 @@ VALUE rb_cUnpacker;
25
25
 
26
26
  // Forward declarations
27
27
  static VALUE rb_packer_alloc(VALUE klass);
28
- static VALUE rb_packer_init(VALUE self);
28
+ static VALUE rb_packer_init(int argc, VALUE* argv, VALUE self);
29
29
  static VALUE rb_unpacker_alloc(VALUE klass);
30
30
  static VALUE rb_unpacker_init(int argc, VALUE* argv, VALUE self);
31
31
  static VALUE rb_pack(VALUE self, VALUE obj);
@@ -37,6 +37,8 @@ typedef struct {
37
37
  int error_occurred; // 0 for no error, 1 if callback encountered an error
38
38
  } HashIterContext;
39
39
 
40
+ static int binary_encindex; // rb_ascii8bit_encindex(), set in Init
41
+
40
42
  // Forward declaration for the recursive function
41
43
  static int pack_ruby_object_recursive(tiny_bits_packer* packer, VALUE obj, VALUE context);
42
44
 
@@ -71,7 +73,7 @@ static void packer_free(void* data) {
71
73
  if (packer_data->packer) {
72
74
  tiny_bits_packer_destroy(packer_data->packer);
73
75
  }
74
- free(packer_data);
76
+ xfree(packer_data);
75
77
  }
76
78
 
77
79
  static size_t packer_memsize(const void* data) {
@@ -90,21 +92,52 @@ static VALUE rb_packer_alloc(VALUE klass) {
90
92
  return TypedData_Wrap_Struct(klass, &packer_data_type, packer_data);
91
93
  }
92
94
 
93
- /*
95
+ // Calls set_dict(target, strs, lens, count) with the #to_s of each dictionary member
96
+ #define WITH_DICT_STRINGS(dict, set_dict, target) do { \
97
+ Check_Type(dict, T_ARRAY); \
98
+ long dict_n = RARRAY_LEN(dict); \
99
+ VALUE dict_strs = rb_ary_new_capa(dict_n); \
100
+ for (long i = 0; i < dict_n; i++) { \
101
+ rb_ary_push(dict_strs, rb_obj_as_string(rb_ary_entry(dict, i))); \
102
+ } \
103
+ VALUE dict_buf; \
104
+ const char **dict_ptrs = ALLOCV(dict_buf, dict_n * (sizeof(char *) + sizeof(size_t)) + 1); \
105
+ size_t *dict_lens = (size_t *)(dict_ptrs + dict_n); \
106
+ for (long i = 0; i < dict_n; i++) { \
107
+ VALUE s = RARRAY_AREF(dict_strs, i); \
108
+ dict_ptrs[i] = RSTRING_PTR(s); \
109
+ dict_lens[i] = RSTRING_LEN(s); \
110
+ } \
111
+ int dict_ok = set_dict(target, dict_ptrs, dict_lens, dict_n); \
112
+ ALLOCV_END(dict_buf); \
113
+ RB_GC_GUARD(dict_strs); \
114
+ if (!dict_ok) rb_raise(rb_eRuntimeError, "Failed to set dictionary"); \
115
+ } while (0)
116
+
117
+ /*
94
118
  * Document-method: initialize
95
119
  *
96
120
  * Initializes a new Packer object
97
121
  *
122
+ * @param dict [Array] strings (or objects responding to #to_s) to deduplicate against without
123
+ * storing them in the output (order is significant), the unpacker must use the same dict
98
124
  * @return [Packer] The initialized packer object.
99
125
  */
100
- static VALUE rb_packer_init(VALUE self) {
126
+ static VALUE rb_packer_init(int argc, VALUE* argv, VALUE self) {
101
127
  PackerData* packer_data;
102
128
  TypedData_Get_Struct(self, PackerData, &packer_data_type, packer_data);
103
129
 
130
+ VALUE opts = Qnil;
131
+ rb_scan_args(argc, argv, ":", &opts);
132
+ VALUE dict = NIL_P(opts) ? Qnil : rb_hash_lookup(opts, ID2SYM(rb_intern("dict")));
133
+
104
134
  packer_data->packer = tiny_bits_packer_create(256, (TB_FEATURE_STRING_DEDUPE | TB_FEATURE_COMPRESS_FLOATS) | 0); // Initial capacity and features
105
135
  if (!packer_data->packer) {
106
136
  rb_raise(rb_eRuntimeError, "Failed to initialize packer");
107
137
  }
138
+ if (!NIL_P(dict)) {
139
+ WITH_DICT_STRINGS(dict, tiny_bits_packer_set_dict, packer_data->packer);
140
+ }
108
141
  return self;
109
142
  }
110
143
 
@@ -112,7 +145,7 @@ static VALUE rb_packer_init(VALUE self) {
112
145
  static inline int pack_ruby_object_recursive(tiny_bits_packer* packer, VALUE obj, VALUE context) {
113
146
  switch (TYPE(obj)) {
114
147
  case T_STRING: {
115
- if (ENCODING_GET_INLINED(obj) == rb_ascii8bit_encindex())
148
+ if (ENCODING_GET_INLINED(obj) == binary_encindex)
116
149
  return pack_blob(packer, RSTRING_PTR(obj), RSTRING_LEN(obj));
117
150
  return pack_str(packer, RSTRING_PTR(obj), RSTRING_LEN(obj));
118
151
  }
@@ -128,13 +161,12 @@ static inline int pack_ruby_object_recursive(tiny_bits_packer* packer, VALUE obj
128
161
  int written = pack_arr(packer, len);
129
162
  if (written <= 0) return 0; // Error check based on tiny_bits API
130
163
  for (long i = 0; i < len; i++) {
131
- if(!pack_ruby_object_recursive(packer, rb_ary_entry(obj, i), context)) return 0; // Propagate error
164
+ if(!pack_ruby_object_recursive(packer, RARRAY_AREF(obj, i), context)) return 0; // Propagate error
132
165
  }
133
166
  return 1; // Success
134
167
  }
135
168
  case T_FIXNUM: {
136
- int64_t val = NUM2LONG(obj); // Assumes fits in int64_t
137
- return pack_int(packer, val);
169
+ return pack_int(packer, (int64_t)FIX2LONG(obj));
138
170
  }
139
171
  case T_BIGNUM: {
140
172
  // within int64: a Bignum only past the Fixnum range (2**62)
@@ -145,8 +177,7 @@ static inline int pack_ruby_object_recursive(tiny_bits_packer* packer, VALUE obj
145
177
  return pack_int(packer, (int64_t)NUM2LL(obj));
146
178
  }
147
179
  case T_FLOAT: {
148
- double val = NUM2DBL(obj);
149
- return pack_double(packer, val);
180
+ return pack_double(packer, RFLOAT_VALUE(obj));
150
181
  }
151
182
  case T_NIL:
152
183
  return pack_null(packer);
@@ -206,7 +237,7 @@ static VALUE rb_pack(VALUE self, VALUE obj) {
206
237
  rb_raise(rb_eRuntimeError, "Failed to pack object (unsupported type or packing error)");
207
238
  }
208
239
 
209
- VALUE result = rb_str_new((const char*)packer_data->packer->buffer, packer_data->packer->current_pos);
240
+ VALUE result = rb_str_new((const char*)tiny_bits_packer_data(packer_data->packer), tiny_bits_packer_size(packer_data->packer));
210
241
  rb_obj_freeze(result);
211
242
  return result;
212
243
  }
@@ -266,10 +297,10 @@ static VALUE rb_to_s(VALUE self){
266
297
  rb_raise(rb_eRuntimeError, "Packer not initialized");
267
298
  }
268
299
 
269
- if(packer_data->packer->current_pos == 0){
300
+ if(tiny_bits_packer_size(packer_data->packer) == 0){
270
301
  return rb_str_new("", 0);
271
302
  }
272
- VALUE result = rb_str_new((const char*)packer_data->packer->buffer, packer_data->packer->current_pos);
303
+ VALUE result = rb_str_new((const char*)tiny_bits_packer_data(packer_data->packer), tiny_bits_packer_size(packer_data->packer));
273
304
  rb_obj_freeze(result);
274
305
  return result;
275
306
  }
@@ -299,19 +330,27 @@ static VALUE rb_reset(VALUE self){
299
330
  typedef struct {
300
331
  tiny_bits_unpacker* unpacker;
301
332
  int symbolize_keys; // map keys that are strings come back as symbols
302
- //size_t strings_index;
303
- //VALUE ruby_strings[TB_HASH_CACHE_SIZE];
333
+ VALUE strings; // Ruby strings by dedupe id, dictionary strings first
334
+ VALUE buffer; // the buffer set by #buffer=, kept alive while popping
304
335
  } UnpackerData;
305
336
 
337
+ static void unpacker_mark(void* data) {
338
+ UnpackerData* unpacker_data = (UnpackerData*)data;
339
+ rb_gc_mark_movable(unpacker_data->strings);
340
+ rb_gc_mark(unpacker_data->buffer); // pinned, the C unpacker points into it (embedded strings move)
341
+ }
342
+
343
+ static void unpacker_compact(void* data) {
344
+ UnpackerData* unpacker_data = (UnpackerData*)data;
345
+ unpacker_data->strings = rb_gc_location(unpacker_data->strings);
346
+ }
347
+
306
348
  static void unpacker_free(void* data) {
307
349
  UnpackerData* unpacker_data = (UnpackerData*)data;
308
350
  if (unpacker_data->unpacker) {
309
351
  tiny_bits_unpacker_destroy(unpacker_data->unpacker);
310
352
  }
311
- //for (size_t i = 0; i < TB_HASH_CACHE_SIZE; i++) {
312
- //unpacker_data->ruby_strings[i] = Qnil;
313
- //}
314
- free(unpacker_data);
353
+ xfree(unpacker_data);
315
354
  }
316
355
 
317
356
  static size_t unpacker_memsize(const void* data) {
@@ -320,7 +359,7 @@ static size_t unpacker_memsize(const void* data) {
320
359
 
321
360
  static const rb_data_type_t unpacker_data_type = {
322
361
  "TinyBits::Unpacker",
323
- {0, unpacker_free, unpacker_memsize,},
362
+ {unpacker_mark, unpacker_free, unpacker_memsize, unpacker_compact,},
324
363
  0, 0, RUBY_TYPED_FREE_IMMEDIATELY
325
364
  };
326
365
 
@@ -328,6 +367,8 @@ static VALUE rb_unpacker_alloc(VALUE klass) {
328
367
  UnpackerData* unpacker_data = ALLOC(UnpackerData);
329
368
  unpacker_data->unpacker = NULL;
330
369
  unpacker_data->symbolize_keys = 0;
370
+ unpacker_data->strings = Qnil;
371
+ unpacker_data->buffer = Qnil;
331
372
  return TypedData_Wrap_Struct(klass, &unpacker_data_type, unpacker_data);
332
373
  }
333
374
 
@@ -337,6 +378,7 @@ static VALUE rb_unpacker_alloc(VALUE klass) {
337
378
  * Initializes a new Unpacker.
338
379
  *
339
380
  * @param symbolize_keys [Boolean] unpack hash keys that are strings as symbols (default false)
381
+ * @param dict [Array] the dictionary the packer used (order is significant)
340
382
  * @return [Unpacker] The initialized unpacker object.
341
383
  */
342
384
  static VALUE rb_unpacker_init(int argc, VALUE* argv, VALUE self) {
@@ -344,9 +386,11 @@ static VALUE rb_unpacker_init(int argc, VALUE* argv, VALUE self) {
344
386
  TypedData_Get_Struct(self, UnpackerData, &unpacker_data_type, unpacker_data);
345
387
 
346
388
  VALUE opts = Qnil;
389
+ VALUE dict = Qnil;
347
390
  rb_scan_args(argc, argv, ":", &opts);
348
391
  if (!NIL_P(opts)) {
349
392
  unpacker_data->symbolize_keys = RTEST(rb_hash_lookup(opts, ID2SYM(rb_intern("symbolize_keys"))));
393
+ dict = rb_hash_lookup(opts, ID2SYM(rb_intern("dict")));
350
394
  }
351
395
 
352
396
  unpacker_data->unpacker = tiny_bits_unpacker_create();
@@ -354,10 +398,22 @@ static VALUE rb_unpacker_init(int argc, VALUE* argv, VALUE self) {
354
398
  rb_raise(rb_eRuntimeError, "Failed to initialize unpacker");
355
399
  }
356
400
  VALUE strings = rb_ary_new_capa(TB_HASH_CACHE_SIZE);
357
- rb_iv_set(self, "@strings", strings);
401
+ if (!NIL_P(dict)) {
402
+ tiny_bits_unpacker* unpacker = unpacker_data->unpacker;
403
+ WITH_DICT_STRINGS(dict, tiny_bits_unpacker_set_dict, unpacker);
404
+ // dictionary strings stay in the first dict_count slots, only later ones are cleared
405
+ for (size_t i = 0; i < unpacker->dict_count; i++) {
406
+ rb_ary_push(strings, rb_enc_interned_str(unpacker->strings[i].str, unpacker->strings[i].length, rb_utf8_encoding()));
407
+ }
408
+ }
409
+ RB_OBJ_WRITE(self, &unpacker_data->strings, strings);
358
410
  return self;
359
411
  }
360
412
 
413
+ static inline void clear_strings(VALUE strings, UnpackerData* unpacker_data) {
414
+ rb_ary_resize(strings, unpacker_data->unpacker->dict_count);
415
+ }
416
+
361
417
  static inline VALUE rb_unpack_str(VALUE strings, UnpackerData* unpacker_data, tiny_bits_value value, size_t interned){
362
418
  int32_t id = value.str_blob_val.id;
363
419
  if(id > 0)
@@ -418,14 +474,22 @@ static VALUE unpack_ruby_object(VALUE strings, UnpackerData* unpacker_data, size
418
474
  }
419
475
  case TINY_BITS_MAP: {
420
476
  VALUE hash = rb_hash_new_capa(value.length);
421
- for (size_t i = 0; i < value.length; i++) {
422
- VALUE key = unpack_ruby_object(strings, unpacker_data, 0);
477
+ VALUE pairs[2 * 16]; // small maps are inserted in one bulk call
478
+ int bulk = value.length <= 16;
479
+ for (size_t i = 0; i < value.length; i++) {
480
+ VALUE key = unpack_ruby_object(strings, unpacker_data, 1);
423
481
  if (key == Qundef) return Qundef; // Error
424
482
  if (unpacker_data->symbolize_keys && RB_TYPE_P(key, T_STRING)) key = rb_str_intern(key);
425
483
  VALUE val = unpack_ruby_object(strings, unpacker_data, 0);
426
484
  if (val == Qundef) return Qundef; // Error
427
- rb_hash_aset(hash, key, val);
485
+ if (bulk) {
486
+ pairs[2 * i] = key;
487
+ pairs[2 * i + 1] = val;
488
+ } else {
489
+ rb_hash_aset(hash, key, val);
490
+ }
428
491
  }
492
+ if (bulk) rb_hash_bulk_insert(2 * value.length, pairs, hash);
429
493
  return hash;
430
494
  }
431
495
  case TINY_BITS_BLOB:
@@ -462,14 +526,18 @@ static VALUE rb_unpack(VALUE self, VALUE buffer) {
462
526
 
463
527
  tiny_bits_unpacker_set_buffer(unpacker_data->unpacker, (const unsigned char*)RSTRING_PTR(buffer), RSTRING_LEN(buffer));
464
528
 
465
- VALUE array = rb_iv_get(self, "@strings");
529
+ VALUE array = unpacker_data->strings;
530
+ clear_strings(array, unpacker_data); // a failed unpack or a pop session may have left entries
466
531
  VALUE result = unpack_ruby_object(array, unpacker_data, 0);
532
+ RB_GC_GUARD(buffer);
533
+ // unpack is for one object only, a later #pop finds an empty buffer (and never reads this one)
534
+ unpacker_data->unpacker->current_pos = unpacker_data->unpacker->size;
467
535
 
468
536
  if (result == Qundef) {
469
537
  rb_raise(rb_eRuntimeError, "Failed to unpack data");
470
538
  }
471
539
 
472
- rb_ary_clear(array);
540
+ clear_strings(array, unpacker_data);
473
541
 
474
542
  return result;
475
543
  }
@@ -492,13 +560,13 @@ static VALUE rb_set_buffer(VALUE self, VALUE buffer){
492
560
  }
493
561
 
494
562
  StringValue(buffer); // Ensure it's a string
563
+ buffer = rb_str_new_frozen(buffer); // the caller mutating their string must not move the bytes we point into
495
564
 
496
565
  tiny_bits_unpacker_set_buffer(unpacker_data->unpacker, (const unsigned char*)RSTRING_PTR(buffer), RSTRING_LEN(buffer));
497
566
 
498
- // set the buffer as an instance variable to mainatin a reference to it
499
- rb_iv_set(self, "@buffer", buffer);
500
- VALUE array = rb_iv_get(self, "@strings");
501
- rb_ary_clear(array);
567
+ // keep a reference to the buffer while popping
568
+ RB_OBJ_WRITE(self, &unpacker_data->buffer, buffer);
569
+ clear_strings(unpacker_data->strings, unpacker_data);
502
570
  return self;
503
571
  }
504
572
 
@@ -512,17 +580,14 @@ static VALUE rb_set_buffer(VALUE self, VALUE buffer){
512
580
  * @note Requires #buffer= to be called first.
513
581
  */
514
582
  static VALUE rb_pop(VALUE self) {
583
+ UnpackerData* unpacker_data;
584
+ TypedData_Get_Struct(self, UnpackerData, &unpacker_data_type, unpacker_data);
515
585
 
516
- VALUE buffer = rb_iv_get(self, "@buffer");
517
- VALUE array = rb_iv_get(self, "@strings");
518
-
519
- if(buffer == Qnil){
586
+ VALUE array = unpacker_data->strings;
587
+ if(unpacker_data->buffer == Qnil){
520
588
  rb_raise(rb_eRuntimeError, "No buffer is set");
521
589
  }
522
590
 
523
- UnpackerData* unpacker_data;
524
- TypedData_Get_Struct(self, UnpackerData, &unpacker_data_type, unpacker_data);
525
-
526
591
  if (!unpacker_data->unpacker) {
527
592
  rb_raise(rb_eRuntimeError, "Unpacker not initialized");
528
593
  }
@@ -530,7 +595,7 @@ static VALUE rb_pop(VALUE self) {
530
595
  tiny_bits_unpacker* unpacker = unpacker_data->unpacker;
531
596
 
532
597
  if(unpacker->current_pos >= unpacker->size){
533
- rb_ary_clear(array);
598
+ clear_strings(array, unpacker_data);
534
599
  return Qnil;
535
600
  }
536
601
 
@@ -540,7 +605,7 @@ static VALUE rb_pop(VALUE self) {
540
605
  }
541
606
 
542
607
  if(unpacker->current_pos >= unpacker->size){
543
- rb_ary_clear(array);
608
+ clear_strings(array, unpacker_data);
544
609
  }
545
610
 
546
611
  return result;
@@ -556,12 +621,11 @@ static VALUE rb_pop(VALUE self) {
556
621
  * @note Requires #buffer= to be called first.
557
622
  */
558
623
  static VALUE rb_finished(VALUE self){
559
- VALUE buffer = rb_iv_get(self, "@buffer");
560
- if(buffer == Qnil){
561
- rb_raise(rb_eRuntimeError, "No buffer is set");
562
- }
563
624
  UnpackerData* unpacker_data;
564
625
  TypedData_Get_Struct(self, UnpackerData, &unpacker_data_type, unpacker_data);
626
+ if(unpacker_data->buffer == Qnil){
627
+ rb_raise(rb_eRuntimeError, "No buffer is set");
628
+ }
565
629
 
566
630
  if (!unpacker_data->unpacker) {
567
631
  rb_raise(rb_eRuntimeError, "Unpacker not initialized");
@@ -577,12 +641,13 @@ static VALUE rb_finished(VALUE self){
577
641
  }
578
642
 
579
643
  void Init_tinybits_ext(void) {
644
+ binary_encindex = rb_ascii8bit_encindex();
580
645
  rb_mTinyBits = rb_define_module("TinyBits");
581
646
  rb_cPacker = rb_define_class_under(rb_mTinyBits, "Packer", rb_cObject);
582
647
  rb_cUnpacker = rb_define_class_under(rb_mTinyBits, "Unpacker", rb_cObject);
583
648
 
584
649
  rb_define_alloc_func(rb_cPacker, rb_packer_alloc);
585
- rb_define_method(rb_cPacker, "initialize", rb_packer_init, 0);
650
+ rb_define_method(rb_cPacker, "initialize", rb_packer_init, -1);
586
651
  rb_define_method(rb_cPacker, "pack", rb_pack, 1);
587
652
  rb_define_alias(rb_cPacker, "encode", "pack");
588
653
  rb_define_alias(rb_cPacker, "dump", "pack");
@@ -1,57 +1,15 @@
1
1
  module TinyBits
2
-
3
- class DPacker
4
-
2
+
3
+ # a Packer with an external dictionary, same as Packer.new(dict: dict)
4
+ class DPacker < Packer
5
+
5
6
  # creates a new TinyBits::DPacker object
6
7
  # @param dict [[String]] an array of strings to use as a dictiontary (order is significant).
7
8
  # @return [TinyBits::DPacker] the dpacker object.
8
9
  # @raise [RuntimeError] the dict is nil or if any member doesn't respnd to to_s.
9
10
  def initialize(dict)
10
- @dict = dict.collect{|a| a.to_s }
11
- @dict_set = false
12
- @packer = Packer.new
13
- end
14
-
15
- def reset
16
- @packer.reset
17
- @dict_set = false
18
- self
19
- end
20
-
21
- # packs an object to a binary string using the stored dictionary
22
- # @param obj [Object] The Ruby object to pack.
23
- # @return [String] The packed binary string (frozen).
24
- # @raise [RuntimeError] If packing fails due to unsupported types or other errors.
25
- def pack(object)
26
- reset
27
- self << object
28
- to_s
29
- end
30
-
31
- # push an object to an inceremental buffer
32
- # @param obj [Object] The Ruby object to pack.
33
- # @return [Integer] The number of bytes written to the buffer.
34
- # @raise [RuntimeError] If packing fails due to unsupported types or other errors.
35
- def <<(object)
36
- if !@dict_set
37
- #@packer.reset
38
- @dict_size = @packer << @dict
39
- @dict_set = true
40
- end
41
- res = @packer << object
42
- res
43
- end
44
-
45
- # return the packed buffer
46
- # @return [String] The packed buffer (minus the dictionary).
47
- def to_s
48
- res = @packer.to_s
49
- if @dict_set
50
- res[@dict_size, res.bytesize - @dict_size]
51
- else
52
- res
53
- end
11
+ super(dict: dict)
54
12
  end
55
13
 
56
14
  end
57
- end
15
+ end
@@ -1,37 +1,14 @@
1
1
  module TinyBits
2
2
 
3
- class DUnpacker
4
-
3
+ # an Unpacker with an external dictionary, same as Unpacker.new(dict: dict)
4
+ class DUnpacker < Unpacker
5
+
5
6
  # @param dict [[String]] the dictionary the packer used (order is significant).
6
7
  # @param symbolize_keys [Boolean] unpack hash keys that are strings as symbols.
7
8
  def initialize(dict, symbolize_keys: false)
8
- @dict = Packer.new.pack(dict.collect{|a| a.to_s })
9
- @unpacker = Unpacker.new(symbolize_keys: symbolize_keys)
10
- @dict_popped = false
11
- end
12
-
13
- def unpack(buffer)
14
- self.buffer = buffer
15
- pop
16
- end
17
-
18
- def buffer=(buffer)
19
- @unpacker.buffer = "#{@dict}#{buffer}"
20
- @dict_popped = false
21
- end
22
-
23
- def pop
24
- if !@dict_popped
25
- @unpacker.pop
26
- @dict_popped = true
27
- end
28
- @unpacker.pop
29
- end
30
-
31
- def finished?
32
- @unpacker.finished?
9
+ super(dict: dict, symbolize_keys: symbolize_keys)
33
10
  end
34
11
 
35
12
  end
36
13
 
37
- end
14
+ end
@@ -1,3 +1,3 @@
1
1
  module TinyBits
2
- VERSION = '0.8.0'.freeze
2
+ VERSION = '0.9.0'.freeze
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tinybits
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.0
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mohamed Hassan