teptris 0.2.51 → 0.2.53

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: 9378e4039e62c8ef1b8271ccf3b2cc8f07ad8db855b4cb399ce4725d25f1d17f
4
- data.tar.gz: b8012bdfb1f3b070f4a42e966da73d9541bce3376a879c252a801af6208b675e
3
+ metadata.gz: ad1b38c6d14d4d34770824e10d1abf84a8b0d16ad7c238e86ad670ec3f8491b1
4
+ data.tar.gz: ebab7849390e490dced8451a67fdc2d9fc15d227dd91620282f7a75f315350a5
5
5
  SHA512:
6
- metadata.gz: 588eb7a305d2c39495de417dd95dd091aa5a3bb1251e62b5feb9e5115c9a9740b202a2d3debc7e8a1934d1ec0459a1d8835c31e01675d5938f59c2be4367d3c9
7
- data.tar.gz: d21e70ded4f0441c8d2f9e5b7d5d7e18d0f6a12c9601f902f70dd4e11a98f154e9f5716cf97839043e2f6247b62af04e6187445523b1b783bd02f231907d0e23
6
+ metadata.gz: ad831f57106754c37c1f0f13ead81e1155e134c96cba2748b95c42baeb6d6c1e99f51cfe0580b3adab6e5def1c2a30fe7636bdb2486be3f3345184815aac4e2a
7
+ data.tar.gz: de27e06e824cb59b96b5ab21eb8a531f1c78bcf5207450b3da6898279795b910b246a260a6420931b2af12f13aa7d59f4f03afd1acbf437ffa9e3f456736f746
@@ -1,4 +1,5 @@
1
1
  #include <ruby.h>
2
+ #include <ruby/thread.h>
2
3
  #include <ruby/encoding.h>
3
4
  #include "teptris/teptris.h"
4
5
  #include "teptris/plan.h"
@@ -237,20 +238,25 @@ static VALUE obj_from_node(const teptris_node *n, unsigned flags,
237
238
  }}
238
239
  }
239
240
 
240
- static teptris_document *parse_or_raise(VALUE str) {
241
- teptris_document *doc = NULL;
242
- teptris_status st = teptris_parse(RSTRING_PTR(str), (size_t)RSTRING_LEN(str), NULL, &doc);
243
- if (st != TEPTRIS_OK) {
244
- const teptris_error *e = teptris_document_error(doc);
245
- VALUE ex = rb_exc_new(eParseError, e->message, (long)strlen(e->message));
246
- rb_iv_set(ex, "@line", SIZET2NUM(e->line));
247
- rb_iv_set(ex, "@column", SIZET2NUM(e->column));
248
- teptris_document_free(doc);
249
- rb_exc_raise(ex);
250
- }
251
- return doc;
241
+ /* the parse is pure C over the buffer with no VM access: run it
242
+ * GVL-free so other threads progress during long parses (and parallel
243
+ * batch loads overlap). The string is locked (pinned) across the
244
+ * release so a concurrent compacting GC cannot relocate the buffer. */
245
+ struct gvl_parse_arg {
246
+ const char *data;
247
+ size_t len;
248
+ teptris_document **doc;
249
+ teptris_status st;
250
+ };
251
+
252
+ static void *parse_without_gvl(void *p)
253
+ {
254
+ struct gvl_parse_arg *a = (struct gvl_parse_arg *)p;
255
+ a->st = teptris_parse(a->data, a->len, NULL, a->doc);
256
+ return NULL;
252
257
  }
253
258
 
259
+
254
260
  static VALUE ext_load(int argc, VALUE *argv, VALUE self) {
255
261
  VALUE str, opts;
256
262
  rb_scan_args(argc, argv, "11", &str, &opts);
@@ -264,11 +270,29 @@ static VALUE ext_load(int argc, VALUE *argv, VALUE self) {
264
270
  if (RTEST(ft)) flags |= FMT_FORBID_TIME;
265
271
  if (RTEST(fd)) flags |= FMT_FORBID_DATE;
266
272
  }
267
- teptris_document *doc = parse_or_raise(str);
273
+ /* the parse is pure C over a private copy, GVL-free; the copy
274
+ * must outlive materialization - the document's strings are
275
+ * zero-copy views into it (a real UAF on glibc when freed early) */
276
+ long len = RSTRING_LEN(str);
277
+ char *cbuf = ALLOC_N(char, (size_t)len + 1);
278
+ memcpy(cbuf, RSTRING_PTR(str), (size_t)len);
279
+ teptris_document *doc = NULL;
280
+ struct gvl_parse_arg arg = {cbuf, (size_t)len, &doc, TEPTRIS_OK};
281
+ rb_thread_call_without_gvl(parse_without_gvl, &arg, RUBY_UBF_IO, NULL);
282
+ if (arg.st != TEPTRIS_OK) {
283
+ const teptris_error *e = teptris_document_error(doc);
284
+ VALUE ex = rb_exc_new(eParseError, e->message, (long)strlen(e->message));
285
+ rb_iv_set(ex, "@line", SIZET2NUM(e->line));
286
+ rb_iv_set(ex, "@column", SIZET2NUM(e->column));
287
+ xfree(cbuf);
288
+ teptris_document_free(doc);
289
+ rb_exc_raise(ex);
290
+ }
268
291
  keycache kc;
269
292
  kc_init(&kc);
270
293
  VALUE out = obj_from_node(teptris_document_root(doc), flags, &kc);
271
294
  kc_free(&kc);
295
+ xfree(cbuf);
272
296
  teptris_document_free(doc);
273
297
  return out;
274
298
  }
@@ -713,7 +737,8 @@ static VALUE cLazyOwner, cLazyValue;
713
737
 
714
738
  typedef struct {
715
739
  teptris_document *doc;
716
- VALUE input; /* the borrowed views' buffer */
740
+ VALUE input; /* single-doc path: owned frozen copy (GC-marked) */
741
+ char *buf; /* batch path: owned input copy the doc's views use */
717
742
  } lazy_owner;
718
743
 
719
744
  typedef struct {
@@ -724,12 +749,18 @@ typedef struct {
724
749
  static void lazy_owner_free(void *p) {
725
750
  lazy_owner *o = (lazy_owner *)p;
726
751
  if (o->doc != NULL) teptris_document_free(o->doc);
752
+ if (o->buf != NULL) xfree(o->buf);
727
753
  xfree(o);
728
754
  }
729
755
  static size_t lazy_owner_size(const void *p) { return sizeof(lazy_owner); }
756
+ static void lazy_owner_mark(void *p)
757
+ {
758
+ lazy_owner *o = (lazy_owner *)p;
759
+ if (o->input) rb_gc_mark(o->input); /* roots the single-path copy */
760
+ }
730
761
  static const rb_data_type_t lazy_owner_type = {
731
762
  "TeptrisExt/LazyOwner",
732
- {NULL, lazy_owner_free, lazy_owner_size, NULL, 0},
763
+ {lazy_owner_mark, lazy_owner_free, lazy_owner_size, NULL, 0},
733
764
  NULL, NULL, RUBY_TYPED_FREE_IMMEDIATELY};
734
765
 
735
766
  static void lazy_value_mark(void *p) {
@@ -764,7 +795,7 @@ static VALUE ext_load_lazy(VALUE self, VALUE toml) {
764
795
  VALUE owner = TypedData_Make_Struct(cLazyOwner, lazy_owner,
765
796
  &lazy_owner_type, o);
766
797
  o->doc = NULL;
767
- o->input = rb_str_new_frozen(toml); /* borrowed views: own a copy */
798
+ o->input = rb_str_new_frozen(toml); /* owned frozen copy */
768
799
  teptris_status st = teptris_parse(RSTRING_PTR(o->input),
769
800
  (size_t)RSTRING_LEN(o->input),
770
801
  NULL, &o->doc);
@@ -945,25 +976,53 @@ static unsigned flags_from_opts(VALUE opts) {
945
976
  /* Parse-only batch (returns an Array of Lazy wrappers, one per doc):
946
977
  * eager parse, deferred materialization. Same per-doc error semantics
947
978
  * as ext_load_lazy. */
979
+ struct batch_parse_arg {
980
+ const char **data;
981
+ size_t *lens;
982
+ size_t n;
983
+ teptris_document **docs;
984
+ teptris_status *statuses;
985
+ teptris_status batch;
986
+ };
987
+
988
+ static void *batch_parse_without_gvl(void *p)
989
+ {
990
+ struct batch_parse_arg *a = (struct batch_parse_arg *)p;
991
+ a->batch = teptris_parse_batch(a->data, a->lens, a->n, NULL,
992
+ a->docs, a->statuses);
993
+ return NULL;
994
+ }
995
+
948
996
  static VALUE ext_load_lazy_batch(VALUE self, VALUE strings) {
949
997
  Check_Type(strings, T_ARRAY);
950
998
  long n = RARRAY_LEN(strings);
951
999
  if (n == 0) return rb_ary_new();
952
1000
  batch_scratch *s = batch_scratch_alloc(n);
953
1001
  if (s == NULL) rb_raise(eError, "out of memory batching %ld documents", n);
1002
+ /* the batch parse is pure C: pin every input (one tmp lock per
1003
+ * string - repeated objects dup; frozen strings cannot take the
1004
+ * lock, yet compaction can move them) and release the GVL for the
1005
+ * whole parse */
1006
+ /* the batch parse is pure C over private copies: a Ruby-string
1007
+ * buffer can move under a concurrent compacting GC, and locktmp
1008
+ * does not pin on every platform - copies are C memory, immovable
1009
+ * by construction. Each LazyOwner owns its copy (freed with the
1010
+ * owner): the lazy nodes view these buffers until materialized. */
1011
+ char **bufs = ALLOC_N(char *, n);
954
1012
  for (long i = 0; i < n; i++) {
955
1013
  VALUE str = rb_ary_entry(strings, i);
956
1014
  StringValue(str); /* raises TypeError if not coercible */
957
- s->data[i] = RSTRING_PTR(str);
958
- s->lens[i] = (size_t)RSTRING_LEN(str);
959
- }
960
- teptris_status batch =
961
- teptris_parse_batch(s->data, s->lens, (size_t)n, NULL,
962
- s->docs, s->statuses);
963
- if (batch != TEPTRIS_OK) {
964
- batch_scratch_free(s);
965
- rb_raise(eError, "batch parse failed (status %d)", (int)batch);
1015
+ long len = RSTRING_LEN(str);
1016
+ bufs[i] = ALLOC_N(char, (size_t)len + 1);
1017
+ memcpy(bufs[i], RSTRING_PTR(str), (size_t)len);
1018
+ s->data[i] = bufs[i];
1019
+ s->lens[i] = (size_t)len;
966
1020
  }
1021
+ struct batch_parse_arg arg = {s->data, s->lens, (size_t)n,
1022
+ s->docs, s->statuses, TEPTRIS_OK};
1023
+ rb_thread_call_without_gvl(batch_parse_without_gvl, &arg,
1024
+ RUBY_UBF_IO, NULL);
1025
+ teptris_status batch = arg.batch;
967
1026
  long first_fail = -1;
968
1027
  for (long i = 0; i < n; i++) {
969
1028
  if (s->statuses[i] != TEPTRIS_OK && first_fail == -1) first_fail = i;
@@ -984,7 +1043,7 @@ static VALUE ext_load_lazy_batch(VALUE self, VALUE strings) {
984
1043
  VALUE owner = TypedData_Make_Struct(cLazyOwner, lazy_owner,
985
1044
  &lazy_owner_type, o);
986
1045
  o->doc = s->docs[i];
987
- o->input = rb_ary_entry(strings, i);
1046
+ o->buf = bufs[i]; /* the owner owns this copy */
988
1047
  rb_ary_push(out, lazy_wrap(owner, teptris_document_root(s->docs[i])));
989
1048
  }
990
1049
  batch_scratch_free(s);
@@ -995,6 +1054,94 @@ static VALUE ext_load_lazy_batch(VALUE self, VALUE strings) {
995
1054
  * as ext_load. opts is parsed once; safe_load semantics apply uniformly.
996
1055
  * The caller's input Array is read-only; its strings stay referenced
997
1056
  * for the duration of the C call (no separate keeper Array). */
1057
+ /* slice-level GVL release: one thread parses K documents under a
1058
+ * single release (no per-document GVL ping-pong), then materializes
1059
+ * them under the GVL with one shared key cache (batch semantics).
1060
+ * All inputs are pinned (dup'd when frozen) across the release. */
1061
+ struct slice_parse_arg {
1062
+ long n;
1063
+ const char **data;
1064
+ size_t *lens;
1065
+ teptris_document **docs;
1066
+ teptris_status *stats;
1067
+ };
1068
+
1069
+ static void *slice_parse_without_gvl(void *p)
1070
+ {
1071
+ struct slice_parse_arg *a = (struct slice_parse_arg *)p;
1072
+ for (long i = 0; i < a->n; i++) {
1073
+ a->docs[i] = NULL;
1074
+ a->stats[i] = teptris_parse(a->data[i], a->lens[i], NULL, &a->docs[i]);
1075
+ }
1076
+ return NULL;
1077
+ }
1078
+
1079
+ static VALUE ext_load_slice(int argc, VALUE *argv, VALUE self) {
1080
+ VALUE strs, opts;
1081
+ rb_scan_args(argc, argv, "11", &strs, &opts);
1082
+ Check_Type(strs, T_ARRAY);
1083
+ long n = RARRAY_LEN(strs);
1084
+ if (n == 0) return rb_ary_new();
1085
+ unsigned flags = flags_from_opts(opts);
1086
+
1087
+ /* the GVL-free parse reads private copies: a Ruby-string buffer
1088
+ * can move under a concurrent compacting GC, and locktmp does not
1089
+ * pin on every platform (windows ruby 3.3 parsed garbage). Copies
1090
+ * are C memory - immovable by construction. Freed after
1091
+ * materialization (the docs view them until then). */
1092
+ const char **data = ALLOC_N(const char *, n);
1093
+ size_t *lens = ALLOC_N(size_t, n);
1094
+ teptris_document **docs = ALLOC_N(teptris_document *, n);
1095
+ teptris_status *stats = ALLOC_N(teptris_status, n);
1096
+ char **bufs = ALLOC_N(char *, n);
1097
+ long i;
1098
+ for (i = 0; i < n; i++) {
1099
+ VALUE str = rb_ary_entry(strs, i);
1100
+ StringValue(str);
1101
+ long len = RSTRING_LEN(str);
1102
+ bufs[i] = ALLOC_N(char, (size_t)len + 1);
1103
+ memcpy(bufs[i], RSTRING_PTR(str), (size_t)len);
1104
+ data[i] = bufs[i];
1105
+ lens[i] = (size_t)len;
1106
+ }
1107
+ struct slice_parse_arg arg = {n, data, lens, docs, stats};
1108
+ rb_thread_call_without_gvl(slice_parse_without_gvl, &arg,
1109
+ RUBY_UBF_IO, NULL);
1110
+
1111
+ VALUE out = rb_ary_new_capa(n);
1112
+ keycache kc;
1113
+ kc_init(&kc);
1114
+ for (i = 0; i < n; i++) {
1115
+ if (stats[i] != TEPTRIS_OK && docs[i] == NULL) {
1116
+ const char *msg = "parse failed (out of memory)";
1117
+ VALUE ex = rb_exc_new(eParseError, msg, (long)strlen(msg));
1118
+ kc_free(&kc);
1119
+ xfree(data); xfree(lens); xfree(docs); xfree(stats);
1120
+ rb_exc_raise(ex);
1121
+ }
1122
+ const teptris_error *e = teptris_document_error(docs[i]);
1123
+ if (stats[i] != TEPTRIS_OK) {
1124
+ VALUE ex = rb_exc_new(eParseError, e->message,
1125
+ (long)strlen(e->message));
1126
+ rb_iv_set(ex, "@line", SIZET2NUM(e->line));
1127
+ rb_iv_set(ex, "@column", SIZET2NUM(e->column));
1128
+ long j;
1129
+ for (j = i; j < n; j++) teptris_document_free(docs[j]);
1130
+ kc_free(&kc);
1131
+ xfree(data); xfree(lens); xfree(docs); xfree(stats);
1132
+ rb_exc_raise(ex);
1133
+ }
1134
+ }
1135
+ for (i = 0; i < n; i++) {
1136
+ rb_ary_store(out, i,
1137
+ obj_from_node(teptris_document_root(docs[i]), flags, &kc));
1138
+ teptris_document_free(docs[i]);
1139
+ }
1140
+ kc_free(&kc);
1141
+ xfree(data); xfree(lens); xfree(docs); xfree(stats);
1142
+ return out;
1143
+ }
1144
+
998
1145
  static VALUE ext_load_batch(int argc, VALUE *argv, VALUE self) {
999
1146
  VALUE strings, opts;
1000
1147
  rb_scan_args(argc, argv, "11", &strings, &opts);
@@ -1052,6 +1199,7 @@ void Init_teptris_ext(void) {
1052
1199
  rb_define_module_function(m, "load", ext_load, -1);
1053
1200
  rb_define_module_function(m, "load_lazy", ext_load_lazy, 1);
1054
1201
  rb_define_module_function(m, "load_batch", ext_load_batch, -1);
1202
+ rb_define_module_function(m, "load_slice", ext_load_slice, -1);
1055
1203
  rb_define_module_function(m, "load_lazy_batch", ext_load_lazy_batch, 1);
1056
1204
  cLazyOwner = rb_define_class_under(m, "LazyOwner", rb_cObject);
1057
1205
  cLazyValue = rb_define_class_under(m, "LazyValue", rb_cObject);
data/lib/teptris/toml.rb CHANGED
@@ -51,9 +51,53 @@ module Teptris
51
51
  # struct across the batch, return one eager Hash per document.
52
52
  # Same datetime contract as load. A failing document raises
53
53
  # Teptris::ParseError carrying the first failure's line/column.
54
- def load_batch(tomls, safe_load: nil, datetime_policy: :native)
54
+ def load_batch(tomls, safe_load: nil, datetime_policy: :native,
55
+ threads: 1)
55
56
  _validate_batch!(tomls, safe_load, datetime_policy)
56
- TeptrisExt.load_batch(tomls, _safe_load_opts(safe_load, datetime_policy))
57
+ opts = _safe_load_opts(safe_load, datetime_policy)
58
+ return TeptrisExt.load_batch(tomls, opts) if threads <= 1
59
+
60
+ # threads > 1: slice across Ruby threads. The C parse releases
61
+ # the GVL, so parses overlap; materialization serializes on the
62
+ # GVL. Contiguous slices keep error order deterministic: the
63
+ # lowest-index failure raises after all threads join.
64
+ n = tomls.size
65
+ threads = [threads, n].min
66
+ return TeptrisExt.load_batch(tomls, opts) if threads <= 1
67
+ # load_slice releases the GVL once per slice (no per-document
68
+ # ping-pong) and shares one key cache across its documents.
69
+ # Repeated objects are dup'd once each HERE: a repeat landing in
70
+ # two slices would double-lock across threads (the slice-level
71
+ # guard cannot see across slices); frozen dup'ing is C-side.
72
+ seen = {}
73
+ tomls = tomls.map do |s|
74
+ next s.dup if seen[s.object_id]
75
+ seen[s.object_id] = true
76
+ s
77
+ end
78
+ per = (n.to_f / threads).ceil
79
+ results = Array.new(n)
80
+ first_error = nil
81
+ mutex = Mutex.new
82
+ threads.times.map do |ti|
83
+ Thread.new(ti) do |i|
84
+ base = i * per
85
+ slice = tomls[base, per] || []
86
+ begin
87
+ loaded = TeptrisExt.load_slice(slice, opts)
88
+ slice.each_with_index { |_, j| results[base + j] = loaded[j] }
89
+ rescue StandardError => e
90
+ mutex.synchronize do
91
+ first_error ||= [base, e]
92
+ end
93
+ end
94
+ end
95
+ end.each(&:join)
96
+ if first_error
97
+ _, err = first_error
98
+ raise err
99
+ end
100
+ results
57
101
  end
58
102
 
59
103
  # Lazy twin of load_batch: parse N inputs eagerly, hand back an
@@ -1,3 +1,3 @@
1
1
  module Teptris
2
- VERSION = "0.2.51"
2
+ VERSION = "0.2.53"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: teptris
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.51
4
+ version: 0.2.53
5
5
  platform: ruby
6
6
  authors:
7
7
  - teptris contributors
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-09-30 00:00:00.000000000 Z
11
+ date: 2026-10-01 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: A native C extension over libteptris, a C99 TOML 1.1 parser/emitter.
14
14
  API shape mirrors the tomlib gem.