pbwire 0.1.7__tar.gz

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.
pbwire-0.1.7/PKG-INFO ADDED
@@ -0,0 +1,13 @@
1
+ Metadata-Version: 2.4
2
+ Name: pbwire
3
+ Version: 0.1.7
4
+ Summary: Dynamic protobuf over the pbwire C ABI, schema at runtime, no generated code
5
+ Home-page: https://github.com/jinganix/pbwire
6
+ Author: jinganix
7
+ License: Apache-2.0
8
+ Requires-Python: >=3.9
9
+ Dynamic: author
10
+ Dynamic: home-page
11
+ Dynamic: license
12
+ Dynamic: requires-python
13
+ Dynamic: summary
pbwire-0.1.7/README.md ADDED
@@ -0,0 +1,170 @@
1
+ # pbwire Python binding
2
+
3
+ `bindings/python/runtime/pbwire.c` is a CPython extension and the Python
4
+ binding: a `FileDescriptorSet` (`protoc --descriptor_set_out`) becomes a schema
5
+ at runtime, and messages go between wire bytes and plain Python data with no
6
+ generated code and no intermediate message tree.
7
+
8
+ ```python
9
+ import pbwire
10
+
11
+ schema = pbwire.Schema.from_file("examples/addressbook.desc")
12
+ person = schema.message("tutorial.Person")
13
+
14
+ wire = person.encode({"name": "alice", "id": 1,
15
+ "phones": [{"number": "555", "type": 1}]})
16
+ message = person.decode(wire) # parsed once, fields read on demand
17
+ assert message.name == "alice"
18
+ assert message.phones[0].number == "555"
19
+ assert message.to_dict() == {"name": "alice", "id": 1,
20
+ "phones": [{"number": "555", "type": 1}]}
21
+ assert person.encode(message) == wire # byte-exact round trip
22
+ ```
23
+
24
+ ## Two representations
25
+
26
+ | | path | cost |
27
+ |---|---|---|
28
+ | `dict` | `encode(dict)` / `Message.to_dict()` | one pass, plain CPython objects |
29
+ | wire | `decode(bytes)` → `Message` | one tag scan, a slice per wire field |
30
+ | `Message` | `encode(message)` | the bytes it was parsed from |
31
+
32
+ `decode()` returns a lazily read message rather than a dict tree, because a
33
+ dict tree has to build every dict, string and int before the caller can look at
34
+ any of them. On a 75 KB message that is ~400 µs of `malloc`-bound object
35
+ creation; `decode()` instead walks the wire once (58 µs) and records where each
36
+ field lives. Field values materialize when they are read, `to_dict()` builds
37
+ the whole tree when it is actually wanted.
38
+
39
+ `decode()` validates everything it walks: every tag, field number, wire type,
40
+ length, nesting depth, sub-message and packed run. A message you get back is
41
+ well-formed; field values (and proto2 `required` presence) are checked when
42
+ they are materialized or written.
43
+
44
+ ## `Message`
45
+
46
+ A `Message` is immutable and keeps the buffer it was parsed from, so sub
47
+ messages share the bytes and `encode(message)` is a copy of them. That makes a
48
+ `decode`/`encode` round trip exact: unknown fields, field order and
49
+ non-canonical encodings survive, which a `to_dict()`/`encode(dict)` round trip
50
+ cannot do. Use `Message` to inspect or pass through payloads, and
51
+ `to_dict()`/`encode()` to work with plain data.
52
+
53
+ | member | meaning |
54
+ |---|---|
55
+ | `msg.name` / `msg["name"]` | field value; attribute access gives `None` for an absent field, item access raises `KeyError` |
56
+ | `msg.nums` | list for a repeated field, built when read |
57
+ | `msg.inner` | sub `Message`; repeated sub-messages come back as a list of `Message` |
58
+ | `msg.get(name, default=None)` | value, or `default` when the field is absent |
59
+ | `msg.keys()` / `values()` / `items()` | fields present on the wire, in wire order |
60
+ | `"nums" in msg`, `len(msg)` | presence and field count |
61
+ | `msg.to_dict()` | the eager form: plain dicts and lists all the way down |
62
+ | `msg.type` | message full name |
63
+ | `encode(msg)` | the message's wire bytes |
64
+
65
+ A singular message field that appears more than once on the wire merges (the
66
+ payloads concatenate), and a oneof member set later clears the earlier ones, in
67
+ both representations.
68
+
69
+ ## Value mapping
70
+
71
+ | protobuf | Python |
72
+ |---|---|
73
+ | int32/int64/sint*/sfixed* | int (signed, range-checked) |
74
+ | uint32/uint64/fixed* | int (range-checked) |
75
+ | bool | bool |
76
+ | float/double | float |
77
+ | string | str (utf-8; undecodable bytes come back as `bytes`) |
78
+ | bytes | bytes |
79
+ | enum | int |
80
+ | message | dict / `Message` |
81
+ | repeated | list (any sequence is accepted on encode) |
82
+
83
+ A field missing from the dict is absent from the message and vice versa, which
84
+ matches proto2 presence and proto3 implicit presence. Values written into the
85
+ dict are emitted as given, including zeros — that is a valid but not the
86
+ shortest encoding of a proto3 message. Unknown fields in the wire are kept by a
87
+ `Message` and skipped by `to_dict()`; an unknown *key* on encode raises
88
+ `KeyError` instead of being dropped silently. Encoding a proto2 message whose
89
+ `required` field is missing raises `ValueError`, and so does materializing one.
90
+
91
+ ## Layout
92
+
93
+ | Piece | Path | Role |
94
+ |---|---|---|
95
+ | runtime | [`runtime/pbwire.c`](runtime/pbwire.c) | the CPython extension: schema, message types, dict <-> wire, parsed messages |
96
+ | examples | [`examples/addressbook.py`](examples/addressbook.py) | schema at runtime, dict round trip + parsed message |
97
+ | tests | [`tests/test_pbwire.py`](tests/test_pbwire.py) | byte parity with the `protoc` reference, oneof/merge/required failure paths |
98
+ | benchmarks | [`benchmarks/bench.py`](benchmarks/bench.py) | vs `google.protobuf` on four tiers |
99
+ | conformance | [`conformance/`](conformance/) | official runner testee, `make conformance-python` |
100
+
101
+ ## Build
102
+
103
+ ```shell
104
+ make python-bind # builds the extension next to the sources
105
+ make python-test # self-check (no dependencies beyond Python)
106
+ make python-bench # benchmark vs google.protobuf (needs protoc + protobuf)
107
+ ```
108
+
109
+ The extension statically links the core (`build/libpbwire.a`), so it needs a C
110
+ compiler and the Python headers (`python3 -c "import sysconfig; ..."`), but no
111
+ setuptools, no wheel, no generated Python. `make python-bind` writes
112
+ `bindings/python/pbwire<EXT_SUFFIX>`; add that directory to `PYTHONPATH` (the
113
+ tests and the benchmark do it themselves).
114
+
115
+ ## Benchmark
116
+
117
+ [`benchmarks/bench.py`](benchmarks/bench.py) compares against `google.protobuf`
118
+ (the upb C extension) on the same `matrix3.proto` message at the four standard
119
+ tiers from [`../../BENCHMARKS.md`](../../BENCHMARKS.md) (small / medium / large /
120
+ xlarge scale the repeated fields x0 / x1 / x100 / x3000), printing the same
121
+ `impl op ns/op MB/s` table as every other harness. Copied from that file, which
122
+ is the single source for all performance numbers; higher is better, MB/s over
123
+ wire bytes.
124
+
125
+ ```shell
126
+ make python-bench
127
+ ```
128
+
129
+ It verifies byte-for-byte parity with `protobuf`'s own encoding and
130
+ both dict representations before timing.
131
+
132
+ | payload | op | pbwire-py (MB/s, x upb) | upb (MB/s) |
133
+ |---|---|---|---|
134
+ | 136 B | encode(data) | 298.9 (5.7x) | 52.3 |
135
+ | 136 B | decode(bytes) | 689.3 (1.20x) | 575.5 |
136
+ | 136 B | to_dict(bytes) | 162.0 (4.9x) | 33.3 |
137
+ | 136 B | encode(parsed) | 3099.8 (4.1x) | 747.6 |
138
+ | 174 B | encode(data) | 342.2 (5.6x) | 60.6 |
139
+ | 174 B | decode(bytes) | 724.9 (1.25x) | 580.8 |
140
+ | 174 B | to_dict(bytes) | 156.7 (5.7x) | 27.6 |
141
+ | 174 B | encode(parsed) | 4005.2 (4.7x) | 860.8 |
142
+ | 2469 B | encode(data) | 495.3 (6.5x) | 76.5 |
143
+ | 2469 B | decode(bytes) | 1167.1 (1.60x) | 730.5 |
144
+ | 2469 B | to_dict(bytes) | 195.9 (4.8x) | 40.4 |
145
+ | 2469 B | encode(parsed) | 30710.1 (22x) | 1400.5 |
146
+ | 74913 B | encode(data) | 564.4 (6.5x) | 86.6 |
147
+ | 74913 B | decode(bytes) | 1366.0 (1.69x) | 810.0 |
148
+ | 74913 B | to_dict(bytes) | 170.6 (3.8x) | 44.6 |
149
+ | 74913 B | encode(parsed) | 97683.0 (63x) | 1540.4 |
150
+
151
+ Rows are the same user-visible task on both sides: `encode(data)` starts from
152
+ the same dict, `decode(bytes)` ends with an in-memory message, `to_dict(bytes)`
153
+ ends with plain Python data, and `encode(parsed)` starts from an already parsed
154
+ message (a `Message` hands back the bytes it was parsed from, so the round trip
155
+ is byte-exact).
156
+
157
+ upb wins nothing: it spends 890 µs filling a message field by field from the
158
+ dict, it parses into its arena and materializes a proxy per field access, and
159
+ its serialization re-encodes every field from its own structs where a parsed
160
+ `Message` just hands back the bytes. The `to_dict()` row is the one where both
161
+ sides pay for every dict, string and int; that cost is `malloc`-bound and
162
+ nobody can avoid it.
163
+
164
+ ## Typed models
165
+
166
+ `protoc-gen-pbwire-typed` (in `../typed`) generates dataclasses with type
167
+ annotations plus `to_dict()`/`from_dict()`, and `encode(msg_type)` /
168
+ `decode(msg_type, wire)` on top of the dict API. The committed output is
169
+ `tests/gen/` and `examples/gen/`; `examples/addressbook.py` uses the dataclasses
170
+ only. See `../typed/README.md`.
pbwire-0.1.7/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.7
@@ -0,0 +1,239 @@
1
+ /* Bump allocator backing pbw_ctx: allocation is a pointer add, releasing is a
2
+ * pointer reset. Chunks double in size so a steady decode workload stops
3
+ * calling malloc entirely after the first few messages. */
4
+
5
+ #include "internal.h"
6
+
7
+ #define PBW_ARENA_MIN_CHUNK 4096u
8
+
9
+ static size_t
10
+ align_up(size_t size)
11
+ {
12
+ /* 8 bytes keeps every struct in this library naturally aligned. */
13
+ return (size + 7u) & ~(size_t)7u;
14
+ }
15
+
16
+ static int
17
+ chunk_add(pbw_arena *arena, size_t need)
18
+ {
19
+ size_t cap = arena->chunk_size;
20
+ pbw_arena_chunk *chunk;
21
+
22
+ if (arena == NULL) {
23
+ return 0;
24
+ }
25
+ if (cap < need) {
26
+ cap = need;
27
+ }
28
+ if (cap > SIZE_MAX - sizeof(*chunk)) {
29
+ return 0;
30
+ }
31
+ chunk = (pbw_arena_chunk *)malloc(sizeof(*chunk) + cap);
32
+ if (chunk == NULL) {
33
+ return 0;
34
+ }
35
+ chunk->next = arena->head;
36
+ chunk->used = 0;
37
+ chunk->cap = cap;
38
+ arena->head = chunk;
39
+ /* Grow the next chunk so a workload that needs more keeps its balance: the
40
+ * chunk count stays small while per-call allocation stays O(1). */
41
+ arena->chunk_size = cap < (size_t)1 << 20 ? cap * 2 : cap;
42
+ return 1;
43
+ }
44
+
45
+ void *
46
+ pbw_arena_alloc_slow(pbw_arena *arena, size_t size)
47
+ {
48
+ pbw_arena_chunk *chunk;
49
+ void *ptr;
50
+
51
+ size_t large_at;
52
+
53
+ if (arena == NULL) {
54
+ return NULL;
55
+ }
56
+ large_at = arena->retain_cap > PBW_ARENA_LARGE_THRESHOLD
57
+ ? arena->retain_cap
58
+ : PBW_ARENA_LARGE_THRESHOLD;
59
+
60
+ if (size > SIZE_MAX - 7u) {
61
+ return NULL;
62
+ }
63
+ size = align_up(size == 0 ? 1 : size);
64
+ if (size > large_at) {
65
+ /* Oversized single allocation: one malloc on the large list, freed on
66
+ * reset/free instead of pinning the reusable head chunk. */
67
+ pbw_arena_chunk *big;
68
+
69
+ /* new_packed guards the same addition; sizes here can approach
70
+ * SIZE_MAX - 7 through the public bytes constructors. */
71
+ if (size > SIZE_MAX - sizeof(*big)) {
72
+ return NULL;
73
+ }
74
+ big = (pbw_arena_chunk *)malloc(sizeof(*big) + size);
75
+
76
+ if (big == NULL) {
77
+ return NULL;
78
+ }
79
+ big->next = arena->large;
80
+ big->used = size;
81
+ big->cap = size;
82
+ arena->large = big;
83
+ return (char *)(big + 1);
84
+ }
85
+ chunk = arena->head;
86
+ if (chunk == NULL || !pbw_cap_holds(chunk->cap, chunk->used) ||
87
+ chunk->cap - chunk->used < size) {
88
+ if (!chunk_add(arena, size)) {
89
+ return NULL;
90
+ }
91
+ chunk = arena->head;
92
+ }
93
+ ptr = (char *)(chunk + 1) + chunk->used;
94
+ chunk->used += size;
95
+ return ptr;
96
+ }
97
+
98
+ void
99
+ pbw_arena_reset(pbw_arena *arena)
100
+ {
101
+ /* Chunks are pushed at the head and grow, so the head is the largest one:
102
+ * keep it and drop the rest. A workload that needs the same amount of space
103
+ * every time then stops calling malloc entirely. */
104
+ pbw_arena_chunk *head;
105
+ pbw_arena_chunk *chunk;
106
+
107
+ if (arena == NULL) {
108
+ return;
109
+ }
110
+ head = arena->head;
111
+
112
+ if (head == NULL || arena->block != NULL) {
113
+ /* A packed arena belongs to one decoded tree and is never reset.
114
+ * A fresh context has no chunks yet: still return it to the blank
115
+ * (mutable) slate. */
116
+ arena->readonly = 0;
117
+ return;
118
+ }
119
+ /* A reset context is a blank slate: trees built into it afterwards are
120
+ * mutable until a decode seals it read-only again. */
121
+ arena->readonly = 0;
122
+ /* Large single allocations never become resident. */
123
+ for (chunk = arena->large; chunk != NULL;) {
124
+ pbw_arena_chunk *next = chunk->next;
125
+
126
+ free(chunk);
127
+ chunk = next;
128
+ }
129
+ arena->large = NULL;
130
+ /* An outlier that grew the head past the budget is dropped as well:
131
+ * steady workloads under budget stay malloc-free, anything above pays
132
+ * one malloc per reset instead of pinning the peak forever. */
133
+ if (head->cap > arena->retain_cap) {
134
+ free(head);
135
+ arena->head = NULL;
136
+ arena->chunk_size = PBW_ARENA_MIN_CHUNK;
137
+ return;
138
+ }
139
+ for (chunk = head->next; chunk != NULL;) {
140
+ pbw_arena_chunk *next = chunk->next;
141
+ free(chunk);
142
+ chunk = next;
143
+ }
144
+ head->next = NULL;
145
+ head->used = 0;
146
+ }
147
+
148
+ pbw_arena *
149
+ pbw_arena_new_packed(size_t first_chunk)
150
+ {
151
+ size_t total;
152
+ uint8_t *block;
153
+ pbw_arena *arena;
154
+ pbw_arena_chunk *chunk;
155
+
156
+ if (first_chunk > SIZE_MAX - sizeof(pbw_arena) - sizeof(pbw_arena_chunk)) {
157
+ return NULL;
158
+ }
159
+ total = sizeof(pbw_arena) + sizeof(pbw_arena_chunk) + first_chunk;
160
+ block = (uint8_t *)malloc(total);
161
+ if (block == NULL) {
162
+ return NULL;
163
+ }
164
+ arena = (pbw_arena *)(void *)block;
165
+ chunk = (pbw_arena_chunk *)(void *)(block + sizeof(pbw_arena));
166
+ chunk->next = NULL;
167
+ chunk->used = 0;
168
+ chunk->cap = first_chunk;
169
+ arena->head = chunk;
170
+ arena->large = NULL;
171
+ arena->retain_cap = PBW_ARENA_LARGE_THRESHOLD;
172
+ arena->chunk_size = first_chunk < PBW_ARENA_MIN_CHUNK ? PBW_ARENA_MIN_CHUNK
173
+ : first_chunk;
174
+ arena->readonly = 0;
175
+ arena->block = block;
176
+ return arena;
177
+ }
178
+
179
+ void
180
+ pbw_arena_free(pbw_arena *arena)
181
+ {
182
+ pbw_arena_chunk *chunk;
183
+ pbw_arena_chunk *big;
184
+
185
+ if (arena == NULL) {
186
+ return;
187
+ }
188
+ chunk = arena->head;
189
+ big = arena->large;
190
+
191
+ while (big != NULL) {
192
+ pbw_arena_chunk *next = big->next;
193
+
194
+ free(big);
195
+ big = next;
196
+ }
197
+ arena->large = NULL;
198
+ if (arena->block != NULL) {
199
+ /* The arena struct and its first chunk live in the block itself. */
200
+ uint8_t *base = (uint8_t *)arena->block;
201
+ pbw_arena_chunk *first =
202
+ (pbw_arena_chunk *)(void *)(base + sizeof(pbw_arena));
203
+
204
+ for (chunk = arena->head; chunk != NULL && chunk != first;) {
205
+ pbw_arena_chunk *next = chunk->next;
206
+
207
+ free(chunk);
208
+ chunk = next;
209
+ }
210
+ free(base);
211
+ return;
212
+ }
213
+ while (chunk != NULL) {
214
+ pbw_arena_chunk *next = chunk->next;
215
+ free(chunk);
216
+ chunk = next;
217
+ }
218
+ arena->head = NULL;
219
+ arena->chunk_size = PBW_ARENA_MIN_CHUNK;
220
+ }
221
+
222
+ size_t
223
+ pbw_arena_used(const pbw_arena *arena)
224
+ {
225
+ const pbw_arena_chunk *chunk;
226
+ size_t used = 0;
227
+
228
+ if (arena == NULL) {
229
+ return 0;
230
+ }
231
+
232
+ for (chunk = arena->head; chunk != NULL; chunk = chunk->next) {
233
+ used += chunk->used;
234
+ }
235
+ for (chunk = arena->large; chunk != NULL; chunk = chunk->next) {
236
+ used += chunk->used;
237
+ }
238
+ return used;
239
+ }
@@ -0,0 +1,286 @@
1
+ /* Reusable per-caller workspace: one bump allocator for decoded messages and one
2
+ * growable buffer for encoded output. This is the explicit, thread-safe-by-
3
+ * construction version of pbwire's per-scheduler thread data. */
4
+
5
+ #include "internal.h"
6
+
7
+ struct pbw_ctx {
8
+ pbw_arena arena;
9
+ pbw_buf out;
10
+ /* Opaque binding slot (Erlang NIF thread data, Go state, ...). Set once
11
+ * by the binding; the core never reads it, and reset/pool keep it. */
12
+ void *user;
13
+ };
14
+
15
+ /* Bounded freelist of contexts. Unsynchronized by design (the core stays
16
+ * C99 with no dependencies): one pool per worker thread, never shared
17
+ * concurrently. Preallocation is lazy: the first acquire allocates, steady
18
+ * use then stops calling malloc. */
19
+ struct pbw_pool {
20
+ pbw_ctx **idle;
21
+ size_t count;
22
+ size_t cap; /* max_idle: how many a release keeps */
23
+ size_t idle_cap; /* allocated slots in idle[] */
24
+ size_t retain_cap; /* budget stamped on contexts this pool creates */
25
+ };
26
+
27
+ int
28
+ pbw_abi_version(void)
29
+ {
30
+ return PBW_ABI_VERSION;
31
+ }
32
+
33
+ pbw_ctx *
34
+ pbw_ctx_new(void)
35
+ {
36
+ pbw_ctx *ctx = (pbw_ctx *)calloc(1, sizeof(*ctx));
37
+
38
+ if (ctx == NULL) {
39
+ return NULL;
40
+ }
41
+ ctx->arena.chunk_size = 4096;
42
+ /* Writable: an empty context holds nothing to protect. Decoding seals
43
+ * it read-only; resetting returns it to this state. */
44
+ ctx->arena.readonly = 0;
45
+ ctx->arena.retain_cap = PBW_ARENA_LARGE_THRESHOLD;
46
+ pbw_buf_init(&ctx->out);
47
+ return ctx;
48
+ }
49
+
50
+ void
51
+ pbw_ctx_free(pbw_ctx *ctx)
52
+ {
53
+ if (ctx == NULL) {
54
+ return;
55
+ }
56
+ pbw_arena_free(&ctx->arena);
57
+ pbw_buf_free(&ctx->out);
58
+ free(ctx);
59
+ }
60
+
61
+ void
62
+ pbw_ctx_set_user(pbw_ctx *ctx, void *user)
63
+ {
64
+ if (ctx != NULL) {
65
+ ctx->user = user;
66
+ }
67
+ }
68
+
69
+ void *
70
+ pbw_ctx_user(const pbw_ctx *ctx)
71
+ {
72
+ return ctx != NULL ? ctx->user : NULL;
73
+ }
74
+
75
+ void
76
+ pbw_ctx_set_retain_cap(pbw_ctx *ctx, size_t cap)
77
+ {
78
+ if (ctx == NULL) {
79
+ return;
80
+ }
81
+ ctx->arena.retain_cap = cap;
82
+ }
83
+
84
+ void
85
+ pbw_ctx_reset(pbw_ctx *ctx)
86
+ {
87
+ if (ctx == NULL) {
88
+ return;
89
+ }
90
+ pbw_arena_reset(&ctx->arena);
91
+ if (ctx->out.cap > ctx->arena.retain_cap) {
92
+ /* Same rule as the arena head: an oversized encode buffer is freed
93
+ * instead of staying resident. A steady workload under budget keeps
94
+ * reusing it with no malloc. */
95
+ pbw_buf_free(&ctx->out);
96
+ }
97
+ ctx->out.len = 0;
98
+ ctx->out.oom = 0;
99
+ }
100
+
101
+ size_t
102
+ pbw_ctx_used(const pbw_ctx *ctx)
103
+ {
104
+ return ctx == NULL ? 0 : pbw_arena_used(&ctx->arena);
105
+ }
106
+
107
+ pbw_pool *
108
+ pbw_pool_new(size_t max_idle, size_t retain_cap)
109
+ {
110
+ pbw_pool *pool = (pbw_pool *)calloc(1, sizeof(*pool));
111
+
112
+ if (pool == NULL) {
113
+ return NULL;
114
+ }
115
+ pool->retain_cap = retain_cap > 0 ? retain_cap
116
+ : PBW_ARENA_LARGE_THRESHOLD;
117
+ pool->cap = max_idle;
118
+ return pool;
119
+ }
120
+
121
+ void
122
+ pbw_pool_free(pbw_pool *pool)
123
+ {
124
+ size_t i;
125
+
126
+ if (pool == NULL) {
127
+ return;
128
+ }
129
+ for (i = 0; i < pool->count; i++) {
130
+ pbw_ctx_free(pool->idle[i]);
131
+ }
132
+ free(pool->idle);
133
+ free(pool);
134
+ }
135
+
136
+ pbw_ctx *
137
+ pbw_pool_acquire(pbw_pool *pool)
138
+ {
139
+ pbw_ctx *ctx;
140
+
141
+ if (pool == NULL) {
142
+ return NULL;
143
+ }
144
+ if (pool->count > 0) {
145
+ return pool->idle[--pool->count];
146
+ }
147
+ ctx = pbw_ctx_new();
148
+ if (ctx == NULL) {
149
+ return NULL;
150
+ }
151
+ pbw_ctx_set_retain_cap(ctx, pool->retain_cap);
152
+ return ctx;
153
+ }
154
+
155
+ void
156
+ pbw_pool_release(pbw_pool *pool, pbw_ctx *ctx)
157
+ {
158
+ pbw_ctx **slot;
159
+
160
+ if (pool == NULL || ctx == NULL) {
161
+ return;
162
+ }
163
+ /* A released context is always clean: the next acquire can use it
164
+ * without resetting. The retain cap bounds what the reset keeps, so
165
+ * an outlier never pins the pool. */
166
+ pbw_ctx_reset(ctx);
167
+ if (pool->count >= pool->cap) {
168
+ pbw_ctx_free(ctx);
169
+ return;
170
+ }
171
+ /* Grow the slot array by doubling: one realloc per doubling instead of
172
+ * one per release, which matters for a pool that fills up. */
173
+ if (pool->count == pool->idle_cap) {
174
+ size_t want;
175
+
176
+ if (pool->idle_cap > SIZE_MAX / 2 / sizeof(*slot)) {
177
+ pbw_ctx_free(ctx);
178
+ return;
179
+ }
180
+ want = pool->idle_cap != 0 ? pool->idle_cap * 2 : 8;
181
+ if (want > pool->cap) {
182
+ want = pool->cap;
183
+ }
184
+ if (want <= pool->idle_cap || want > SIZE_MAX / sizeof(*slot)) {
185
+ pbw_ctx_free(ctx);
186
+ return;
187
+ }
188
+ slot = (pbw_ctx **)realloc(pool->idle, want * sizeof(*slot));
189
+ if (slot == NULL) {
190
+ pbw_ctx_free(ctx);
191
+ return;
192
+ }
193
+ pool->idle = slot;
194
+ pool->idle_cap = want;
195
+ }
196
+ pool->idle[pool->count++] = ctx;
197
+ }
198
+
199
+ size_t
200
+ pbw_pool_idle(const pbw_pool *pool)
201
+ {
202
+ return pool == NULL ? 0 : pool->count;
203
+ }
204
+
205
+ pbw_status
206
+ pbw_decode_ctx(pbw_ctx *ctx, const pbw_descriptor *desc, const void *buf,
207
+ size_t len, pbw_message **out)
208
+ {
209
+ return pbw_decode_ctx_limits(ctx, desc, buf, len, NULL, out);
210
+ }
211
+
212
+ pbw_status
213
+ pbw_decode_ctx_limits(pbw_ctx *ctx, const pbw_descriptor *desc,
214
+ const void *buf, size_t len, const pbw_limits *limits,
215
+ pbw_message **out)
216
+ {
217
+ if (out != NULL) {
218
+ *out = NULL;
219
+ }
220
+ if (ctx == NULL || out == NULL || (buf == NULL && len > 0)) {
221
+ return PBW_ERR_INVALID;
222
+ }
223
+ /* Every failure below leaves NULL: callers can rely on *out alone. */
224
+ if (limits != NULL && limits->max_bytes > 0 && len > limits->max_bytes) {
225
+ return PBW_ERR_LIMIT;
226
+ }
227
+ {
228
+ pbw_status status =
229
+ pbw_decode_arena_lim(&ctx->arena, desc, buf, len, pbw_effective_depth(limits), out);
230
+ if (status == PBW_OK) {
231
+ /* Seal the arena: decoded trees are read-only, later builds
232
+ * need a reset first. */
233
+ ctx->arena.readonly = 1;
234
+ }
235
+ return status;
236
+ }
237
+ }
238
+
239
+ pbw_status
240
+ pbw_encode_ctx(pbw_ctx *ctx, const pbw_message *msg, const uint8_t **out,
241
+ size_t *len)
242
+ {
243
+ pbw_status status;
244
+
245
+ if (ctx == NULL || out == NULL || len == NULL) {
246
+ return PBW_ERR_INVALID;
247
+ }
248
+ *out = NULL;
249
+ *len = 0;
250
+ status = pbw_encode_buf(&ctx->out, msg);
251
+ if (status != PBW_OK) {
252
+ *out = NULL;
253
+ *len = 0;
254
+ return status;
255
+ }
256
+ *out = ctx->out.data != NULL ? ctx->out.data : (const uint8_t *)"";
257
+ *len = ctx->out.len;
258
+ return PBW_OK;
259
+ }
260
+
261
+ pbw_message *
262
+ pbw_message_new_ctx(pbw_ctx *ctx, const pbw_descriptor *desc)
263
+ {
264
+ if (ctx == NULL) {
265
+ return NULL;
266
+ }
267
+ return pbw_message_new_a(&ctx->arena, desc);
268
+ }
269
+
270
+ pbw_value *
271
+ pbw_value_new_uint_ctx(pbw_ctx *ctx, uint64_t bits)
272
+ {
273
+ if (ctx == NULL) {
274
+ return NULL;
275
+ }
276
+ return pbw_value_new_uint_a(&ctx->arena, bits);
277
+ }
278
+
279
+ pbw_value *
280
+ pbw_value_new_bytes_ctx(pbw_ctx *ctx, const void *data, size_t len)
281
+ {
282
+ if (ctx == NULL) {
283
+ return NULL;
284
+ }
285
+ return pbw_value_new_bytes_a(&ctx->arena, data, len);
286
+ }