pbwire 0.1.1
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 +7 -0
- data/.diffend-unextracted-0 +32 -0
- data/README.md +223 -0
- data/ext/pbwire_ext/extconf.rb +15 -0
- data/ext/pbwire_ext/pbwire_ext.c +1836 -0
- data/lib/pbwire.rb +52 -0
- metadata +57 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 2dbe851b0f4083848df9a1741ae373a0fe11438a1265a448d63a48e57058f5e2
|
|
4
|
+
data.tar.gz: 4d57de289cfbdbb516a08070ef806ae991a5a3a735345e1e44e8b81b0b8fd5ca
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: b6888351a8032c60dd9bf0baafdb3cdf185faf04f86834bfa13f50042971e2f1e4ae89a5b7605bbdf98e30507e4c6335868e8a8f72f749d124ef01960bd41cbb
|
|
7
|
+
data.tar.gz: f0a0b84bdc336c8ce83398e4d7a6ea9d35a3d9b6c94ff8c4e0440734cdeeebb9850b1378a1093781c629ad706a83b963288ebe3f56625e53dd985a06ec0900e8
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[diffend] Unextracted member
|
|
2
|
+
source: tar
|
|
3
|
+
skipped a member with an empty or parent path: '../../VERSION'
|
|
4
|
+
skipped a member with an empty or parent path: '../../include/pbwire.h'
|
|
5
|
+
skipped a member with an empty or parent path: '../../src/arena.c'
|
|
6
|
+
skipped a member with an empty or parent path: '../../src/ctx.c'
|
|
7
|
+
skipped a member with an empty or parent path: '../../src/decode.c'
|
|
8
|
+
skipped a member with an empty or parent path: '../../src/descriptor_set.c'
|
|
9
|
+
skipped a member with an empty or parent path: '../../src/encode.c'
|
|
10
|
+
skipped a member with an empty or parent path: '../../src/json.c'
|
|
11
|
+
skipped a member with an empty or parent path: '../../src/message.c'
|
|
12
|
+
skipped a member with an empty or parent path: '../../src/schema.c'
|
|
13
|
+
skipped a member with an empty or parent path: '../../src/stream.c'
|
|
14
|
+
skipped a member with an empty or parent path: '../../src/text.c'
|
|
15
|
+
skipped a member with an empty or parent path: '../../src/wire.c'
|
|
16
|
+
|
|
17
|
+
GNU tar output:
|
|
18
|
+
tar: Removing leading `../../' from member names
|
|
19
|
+
tar: ../../VERSION: Member name contains '..'
|
|
20
|
+
tar: ../../include/pbwire.h: Member name contains '..'
|
|
21
|
+
tar: ../../src/arena.c: Member name contains '..'
|
|
22
|
+
tar: ../../src/ctx.c: Member name contains '..'
|
|
23
|
+
tar: ../../src/decode.c: Member name contains '..'
|
|
24
|
+
tar: ../../src/descriptor_set.c: Member name contains '..'
|
|
25
|
+
tar: ../../src/encode.c: Member name contains '..'
|
|
26
|
+
tar: ../../src/json.c: Member name contains '..'
|
|
27
|
+
tar: ../../src/message.c: Member name contains '..'
|
|
28
|
+
tar: ../../src/schema.c: Member name contains '..'
|
|
29
|
+
tar: ../../src/stream.c: Member name contains '..'
|
|
30
|
+
tar: ../../src/text.c: Member name contains '..'
|
|
31
|
+
tar: ../../src/wire.c: Member name contains '..'
|
|
32
|
+
tar: Exiting with failure status due to previous errors
|
data/README.md
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
# pbwire Ruby binding
|
|
2
|
+
|
|
3
|
+
`lib/pbwire.rb` loads a compiled C extension (`ext/pbwire_ext/`) that links the
|
|
4
|
+
pbwire C core statically. No gems, no `protoc` plugin, no generated code: a
|
|
5
|
+
`FileDescriptorSet` (`protoc --descriptor_set_out`) is loaded at runtime and
|
|
6
|
+
drives every field lookup, so the field numbers, types and presence rules are the
|
|
7
|
+
descriptor's, not a hand-written copy.
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
require 'pbwire'
|
|
11
|
+
|
|
12
|
+
schema = Pbwire::Schema.from_file("sample.desc")
|
|
13
|
+
sample = schema.message("pbwire.ruby.Sample") # a builder
|
|
14
|
+
|
|
15
|
+
sample["i32"] = -7
|
|
16
|
+
sample["name"] = "alice"
|
|
17
|
+
sample["nums"] = [1, 2, 3] # repeated: assign the whole Array
|
|
18
|
+
inner = schema.message("pbwire.ruby.Inner")
|
|
19
|
+
inner["s"] = "nested"
|
|
20
|
+
sample["inner"] = inner # hands the tree to the parent
|
|
21
|
+
|
|
22
|
+
wire = sample.encode # binary String
|
|
23
|
+
back = schema.message("pbwire.ruby.Sample").decode(wire)
|
|
24
|
+
|
|
25
|
+
back["name"] # => "alice"
|
|
26
|
+
back["nums"] # => [1, 2, 3]
|
|
27
|
+
back["inner"]["s"] # => "nested"
|
|
28
|
+
back.encode == wire # => true: byte-exact re-encode
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Build and run
|
|
32
|
+
|
|
33
|
+
The extension is compiled once per interpreter with `sh build.sh` (or
|
|
34
|
+
`make ruby-bind`), which uses that Ruby's own headers and links `build/libpbwire.a`
|
|
35
|
+
into `lib/pbwire_ext.<DLEXT>`. Nothing else is needed at run time -- no shared
|
|
36
|
+
library path, no gem:
|
|
37
|
+
|
|
38
|
+
```shell
|
|
39
|
+
make ruby-bind # compiles lib/pbwire_ext.bundle for $(RUBY)
|
|
40
|
+
make ruby-test # builds, then runs test/test_pbwire.rb
|
|
41
|
+
ruby -Ibindings/ruby/lib bindings/ruby/examples/roundtrip.rb
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`RUBY=` selects the interpreter, both for the build and the benchmark:
|
|
45
|
+
|
|
46
|
+
```shell
|
|
47
|
+
RUBY=/opt/homebrew/bin/ruby make ruby-test
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The test prints one `ok`/`FAIL` line per check and exits non-zero on a failure;
|
|
51
|
+
`test/data/sample.desc` is checked in, so no `protoc` is needed at test time.
|
|
52
|
+
|
|
53
|
+
## Two message representations
|
|
54
|
+
|
|
55
|
+
Both are `Pbwire::Message`; the class of the object tells you which one you have.
|
|
56
|
+
|
|
57
|
+
- **A builder** comes from `schema.message(name)`. It is mutable: `set`/`[]=` store
|
|
58
|
+
into a C message tree, `get`/`[]` read it back, and `encode` walks it with the C
|
|
59
|
+
core, writing straight into the returned String (one pass, no intermediate
|
|
60
|
+
buffer).
|
|
61
|
+
- **A wire view** comes from `msg.decode(bytes)`. It is a read-only view over the
|
|
62
|
+
bytes: one validating tag scan records where each wire field lives, `get`
|
|
63
|
+
materializes only the field asked for, and `encode` hands the exact bytes back,
|
|
64
|
+
so unknown fields, field order and non-canonical encodings survive a
|
|
65
|
+
decode/encode round trip. `set` on a wire view raises: build a message and
|
|
66
|
+
encode it to change data.
|
|
67
|
+
|
|
68
|
+
That split is why the binding is fast: decoding 3000 repeated items costs one
|
|
69
|
+
scan rather than 3000 message trees, and encoding a builder is one pass into the
|
|
70
|
+
caller's string. Field facts (number, kind, wire type, packed/required/oneof,
|
|
71
|
+
sub type, name lookup) are resolved once per message type and cached per schema.
|
|
72
|
+
|
|
73
|
+
## API
|
|
74
|
+
|
|
75
|
+
| member | meaning |
|
|
76
|
+
|---|---|
|
|
77
|
+
| `Pbwire::Schema.from_file(path)` / `.from_bytes(string)` | load a `FileDescriptorSet`; raises `Pbwire::Error` with the core's `pbw_schema_error()` text when the set is rejected |
|
|
78
|
+
| `Pbwire::Schema.from_file(path) { \|schema\| ... }` (same for `.from_bytes`) | block form: the schema closes itself after the block, even on exception, like `File.open`; without a block the caller owns it and closes explicitly |
|
|
79
|
+
| `schema.message("pkg.Name")` | a new builder message; raises `Pbwire::Error` for an unknown name |
|
|
80
|
+
| `msg.set(name_or_number_symbol, value)` / `msg[...] = value` | set a field; returns the message |
|
|
81
|
+
| `msg.get(name_or_number_symbol)` / `msg[...]` | field value; `nil` when a singular field is absent, `[]` for an empty repeated field |
|
|
82
|
+
| `msg.type` | full message name |
|
|
83
|
+
| `msg.encode` | wire bytes as a binary `String` |
|
|
84
|
+
| `msg.decode(bytes)` | a wire view of the bytes; unknown fields and field order survive, so `decode(wire).encode == wire` |
|
|
85
|
+
| `msg.close` / `schema.close` | free the C object now; idempotent, also `release`. A schema is freed once its last message is gone |
|
|
86
|
+
| `msg.closed?` / `schema.closed?` | whether it was released |
|
|
87
|
+
|
|
88
|
+
Every `pbw_status` is checked and raised as `Pbwire::Error`; a wrong Ruby type
|
|
89
|
+
for a value raises `ArgumentError`/`TypeError`. Nothing aborts or segfaults on a
|
|
90
|
+
bad call. Malformed wire data is rejected by `decode` (every tag, length and
|
|
91
|
+
packed run is checked, nesting is capped, and proto2 required fields are
|
|
92
|
+
enforced).
|
|
93
|
+
|
|
94
|
+
### Value mapping
|
|
95
|
+
|
|
96
|
+
| protobuf | Ruby |
|
|
97
|
+
|---|---|
|
|
98
|
+
| int32/int64/sint*/sfixed*/enum | Integer (signed 64-bit range) |
|
|
99
|
+
| uint32/uint64/fixed* | Integer (unsigned 64-bit range) |
|
|
100
|
+
| bool | `true` / `false` (also accepts `nil` as `false`) |
|
|
101
|
+
| float/double | Float (Integers are accepted) |
|
|
102
|
+
| string | String, UTF-8 tagged (undecodable bytes stay as-is and are not lost) |
|
|
103
|
+
| bytes | String, `Encoding::BINARY` |
|
|
104
|
+
| message | `Pbwire::Message`, set by handing a builder over |
|
|
105
|
+
| repeated field | Array; assign a new Array to replace it, read a new Array back |
|
|
106
|
+
|
|
107
|
+
A field that was not set or not on the wire reads as `nil`, and a field set to
|
|
108
|
+
`false`/`0` is present and round trips as such (explicit presence, like the C
|
|
109
|
+
API). Assigning a repeated field replaces its contents (`[]` clears it); reading
|
|
110
|
+
one returns a copy, so `msg["nums"] << 1` does not modify the message.
|
|
111
|
+
|
|
112
|
+
A builder handed to a message field moves into that message and is closed
|
|
113
|
+
afterwards (`closed?` is true). The same builder cannot be handed over twice, and
|
|
114
|
+
a message cannot contain itself.
|
|
115
|
+
|
|
116
|
+
### Memory
|
|
117
|
+
|
|
118
|
+
A `Schema` owns one C schema and a message owns its C tree (a builder) or its
|
|
119
|
+
slice index (a wire view); an abandoned object is released by a GC finalizer,
|
|
120
|
+
and `#close` releases it deterministically. Closing twice cannot double free.
|
|
121
|
+
|
|
122
|
+
`pbw_message_free()` reads the descriptor, so the schema's C memory has to
|
|
123
|
+
outlive its last message -- and Ruby promises no finalizer order. A refcounted
|
|
124
|
+
holder therefore reference-counts the schema's users: whichever side is released
|
|
125
|
+
last frees the schema, and `schema.close` with messages still alive frees it when
|
|
126
|
+
the last one goes instead of leaving the messages dangling.
|
|
127
|
+
|
|
128
|
+
A wire view keeps a frozen shared copy of the bytes it was decoded from (Ruby's
|
|
129
|
+
copy-on-write makes this free for large strings), so later writes to the input
|
|
130
|
+
String do not change the message. A borrowed sub-message returned by `get` is
|
|
131
|
+
owned by its parent: it keeps the parent alive and refuses to be used after the
|
|
132
|
+
parent is closed.
|
|
133
|
+
|
|
134
|
+
## Repeated fields, maps, groups
|
|
135
|
+
|
|
136
|
+
Repeated scalars (packed or unpacked on the wire), repeated strings/bytes and
|
|
137
|
+
repeated messages are supported; a map field reads as an Array of entry
|
|
138
|
+
messages. Groups (proto2 legacy) are not exposed: `get`/`set` raise, and the wire
|
|
139
|
+
view keeps their bytes as unknown fields (so a round trip still preserves them).
|
|
140
|
+
Extensions are read by number, not name.
|
|
141
|
+
|
|
142
|
+
## Deliberately skipped
|
|
143
|
+
|
|
144
|
+
- **No `protoc` plugin, no codegen.** The descriptor set is the schema; nothing
|
|
145
|
+
is generated and there is no hand-built schema path.
|
|
146
|
+
- **No JSON/text format, no deterministic encoding, no streaming.** All of those
|
|
147
|
+
exist in the C API and other bindings; this one stays the smallest thing that
|
|
148
|
+
reads and writes binary messages.
|
|
149
|
+
|
|
150
|
+
## Benchmark
|
|
151
|
+
|
|
152
|
+
`benchmarks/bench.sh` runs the four standard tiers from
|
|
153
|
+
[`BENCHMARKS.md`](../../BENCHMARKS.md) against the `google-protobuf` gem on the
|
|
154
|
+
same `matrix3.proto` (small / medium / large / xlarge scale the repeated fields
|
|
155
|
+
x0 / x1 / x100 / x3000). Both sides build the same message and must produce
|
|
156
|
+
identical bytes before anything is timed; the benchmark
|
|
157
|
+
interleaves the implementations and keeps the best of several runs. It needs
|
|
158
|
+
`protoc`, a C compiler and the gem. Copied from that file, which is the single
|
|
159
|
+
source for all performance numbers; higher is better, MB/s over wire bytes:
|
|
160
|
+
|
|
161
|
+
| payload | op | pbwire-ruby (MB/s, x gem) | gem (MB/s) |
|
|
162
|
+
|---|---|---|---|
|
|
163
|
+
| 136 B | encode | 896.1 (1.29x) | 695.9 |
|
|
164
|
+
| 136 B | decode | 392.8 (1.61x) | 243.4 |
|
|
165
|
+
| 174 B | encode | 858.1 (1.11x) | 771.0 |
|
|
166
|
+
| 174 B | decode | 416.4 (1.68x) | 247.5 |
|
|
167
|
+
| 2469 B | encode | 1418.6 (1.16x) | 1222.6 |
|
|
168
|
+
| 2469 B | decode | 924.3 (1.88x) | 492.1 |
|
|
169
|
+
| 74913 B | encode | 1607.8 (1.22x) | 1312.4 |
|
|
170
|
+
| 74913 B | decode | 1122.2 (1.67x) | 673.3 |
|
|
171
|
+
|
|
172
|
+
```shell
|
|
173
|
+
gem install google-protobuf
|
|
174
|
+
RUBY=$(command -v ruby) make ruby-bench
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
The win is architectural, not a micro-optimization:
|
|
178
|
+
|
|
179
|
+
- **Encoding** writes one pass from the C tree straight into the String that is
|
|
180
|
+
returned; there is no intermediate buffer and no per-field Ruby call.
|
|
181
|
+
- **Decoding** is one validating scan that records where each wire field lives;
|
|
182
|
+
the gem materializes every field, which is what the 0.44-0.69x numbers mean.
|
|
183
|
+
|
|
184
|
+
## Conformance
|
|
185
|
+
|
|
186
|
+
`conformance/testee.rb` drives the binary wire format of the official
|
|
187
|
+
`conformance_test_runner` through this binding (`make conformance-ruby`, reusing
|
|
188
|
+
the cached protobuf checkout from `make conformance-setup`):
|
|
189
|
+
|
|
190
|
+
```
|
|
191
|
+
CONFORMANCE SUITE PASSED: 1199 successes, 1403 skipped, 0 expected failures,
|
|
192
|
+
0 unexpected failures.
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
JSON/text/JSPB requests answer `skipped` (the binding has no printers); the
|
|
196
|
+
binary verdicts match the reference implementation exactly.
|
|
197
|
+
|
|
198
|
+
## Layout
|
|
199
|
+
|
|
200
|
+
| Piece | Path |
|
|
201
|
+
|---|---|
|
|
202
|
+
| the binding | [`lib/pbwire.rb`](lib/pbwire.rb) loads the extension |
|
|
203
|
+
| the extension | [`ext/pbwire_ext/pbwire_ext.c`](ext/pbwire_ext/pbwire_ext.c) |
|
|
204
|
+
| build | [`build.sh`](build.sh) |
|
|
205
|
+
| example | [`examples/roundtrip.rb`](examples/roundtrip.rb) |
|
|
206
|
+
| self-check | [`test/test_pbwire.rb`](test/test_pbwire.rb) |
|
|
207
|
+
| benchmark | [`benchmarks/bench.rb`](benchmarks/bench.rb) | the four standard tiers against the `google-protobuf` gem |
|
|
208
|
+
| conformance | [`conformance/testee.rb`](conformance/testee.rb) | binary wire format through the official runner |
|
|
209
|
+
| fixture | [`test/data/sample.proto`](test/data/sample.proto) and the checked-in `sample.desc` |
|
|
210
|
+
|
|
211
|
+
Regenerate the descriptor set after editing the fixture (it is checked in, so the
|
|
212
|
+
tests need no `protoc`):
|
|
213
|
+
|
|
214
|
+
```shell
|
|
215
|
+
(cd bindings/ruby/test/data && protoc --descriptor_set_out=sample.desc sample.proto)
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## Typed models
|
|
219
|
+
|
|
220
|
+
`protoc-gen-pbwire-typed` (in `../typed`) generates classes with keyword-argument
|
|
221
|
+
constructors and accessors, `#encode(schema)` and `.decode(schema, wire)`. The
|
|
222
|
+
committed output is `test/gen/` and `examples/gen/`; `examples/roundtrip.rb`
|
|
223
|
+
uses typed assignments only. See `../typed/README.md`.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Builds the C extension for rubygems: compiles ext/pbwire_ext/pbwire_ext.c
|
|
4
|
+
# together with the C core (../../../src/*.c), so the gem is self-contained.
|
|
5
|
+
require "mkmf"
|
|
6
|
+
|
|
7
|
+
root = File.expand_path("../../../..", __dir__)
|
|
8
|
+
core = %w[wire arena schema message encode decode ctx descriptor_set text json stream]
|
|
9
|
+
srcs = [File.join(__dir__, "pbwire_ext.c")] + core.map { |c| File.join(root, "src", "#{c}.c") }
|
|
10
|
+
|
|
11
|
+
$INCFLAGS += " -I#{File.join(root, "include")}"
|
|
12
|
+
$srcs = srcs
|
|
13
|
+
$objs = srcs.map { |s| s.sub(/\.c$/, ".o") }
|
|
14
|
+
|
|
15
|
+
create_makefile("pbwire_ext")
|