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 +13 -0
- pbwire-0.1.7/README.md +170 -0
- pbwire-0.1.7/VERSION +1 -0
- pbwire-0.1.7/core/arena.c +239 -0
- pbwire-0.1.7/core/ctx.c +286 -0
- pbwire-0.1.7/core/decode.c +1397 -0
- pbwire-0.1.7/core/descriptor_set.c +862 -0
- pbwire-0.1.7/core/encode.c +1268 -0
- pbwire-0.1.7/core/internal.h +483 -0
- pbwire-0.1.7/core/json.c +4446 -0
- pbwire-0.1.7/core/message.c +990 -0
- pbwire-0.1.7/core/schema.c +889 -0
- pbwire-0.1.7/core/stream.c +234 -0
- pbwire-0.1.7/core/text.c +2511 -0
- pbwire-0.1.7/core/wire.c +398 -0
- pbwire-0.1.7/include/pbwire-protoc/descriptor_set.h +45 -0
- pbwire-0.1.7/include/pbwire-protoc/error.h +25 -0
- pbwire-0.1.7/include/pbwire-protoc/importer.h +64 -0
- pbwire-0.1.7/include/pbwire.h +534 -0
- pbwire-0.1.7/pbwire.egg-info/PKG-INFO +13 -0
- pbwire-0.1.7/pbwire.egg-info/SOURCES.txt +21 -0
- pbwire-0.1.7/pbwire.egg-info/dependency_links.txt +1 -0
- pbwire-0.1.7/pbwire.egg-info/top_level.txt +1 -0
- pbwire-0.1.7/pyproject.toml +3 -0
- pbwire-0.1.7/runtime/pbwire.c +2581 -0
- pbwire-0.1.7/setup.cfg +4 -0
- pbwire-0.1.7/setup.py +82 -0
- pbwire-0.1.7/tests/test_pbwire.py +220 -0
- pbwire-0.1.7/tests/test_typed.py +89 -0
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
|
+
}
|
pbwire-0.1.7/core/ctx.c
ADDED
|
@@ -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
|
+
}
|