teptris 0.2.51-aarch64-linux → 0.2.53-aarch64-linux
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 +4 -4
- data/ext/teptris_ext/materialize.c +174 -26
- data/lib/teptris/3.3/teptris_ext.so +0 -0
- data/lib/teptris/3.4/teptris_ext.so +0 -0
- data/lib/teptris/4.0/teptris_ext.so +0 -0
- data/lib/teptris/toml.rb +46 -2
- data/lib/teptris/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 168a5dbc5e797d21c3d6798cd78b90d4b8a704b1f9423413904c71b2559c31fb
|
|
4
|
+
data.tar.gz: 2e6f06fcfc14d22db8c821402462d428a09dd30c4c63e4ac5d071173415084a8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 050ed9059cefb4cc60b37194b353e9a385335aa22e99395ed993833967efa7d466e0e3c0de2d9764ee08b528f56cf6e7e622aa6f3d0f0c5503931a395746ea55
|
|
7
|
+
data.tar.gz: 4c24b94cbe121c7413367f42afd48413e2acce70dd47328614d9f7da94735d91b74cfdbc30d44404e8b46f89519fa75a2e7ad64fd30f501c07d3798e10a5fd32
|
|
@@ -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
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
-
|
|
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; /*
|
|
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
|
-
{
|
|
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); /*
|
|
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
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
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->
|
|
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);
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
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
|
-
|
|
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
|
data/lib/teptris/version.rb
CHANGED
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.
|
|
4
|
+
version: 0.2.53
|
|
5
5
|
platform: aarch64-linux
|
|
6
6
|
authors:
|
|
7
7
|
- teptris contributors
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
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.
|