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 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")