File: design.md

package info (click to toggle)
chromium 139.0.7258.127-1
  • links: PTS, VCS
  • area: main
  • in suites:
  • size: 6,122,068 kB
  • sloc: cpp: 35,100,771; ansic: 7,163,530; javascript: 4,103,002; python: 1,436,920; asm: 946,517; xml: 746,709; pascal: 187,653; perl: 88,691; sh: 88,436; objc: 79,953; sql: 51,488; cs: 44,583; fortran: 24,137; makefile: 22,147; tcl: 15,277; php: 13,980; yacc: 8,984; ruby: 7,485; awk: 3,720; lisp: 3,096; lex: 1,327; ada: 727; jsp: 228; sed: 36
file content (429 lines) | stat: -rw-r--r-- 19,399 bytes parent folder | download | duplicates (5)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
# upb Design

[TOC]

upb is a protobuf kernel written in C. It is a fast and conformant
implementation of protobuf, with a low-level C API that is designed to be
wrapped in other languages.

upb is not designed to be used by applications directly. The C API is very
low-level, unsafe, and changes frequently. It is important that upb is able to
make breaking API changes as necessary, to avoid taking on technical debt that
would compromise upb's goals of small code size and fast performance.

## Design goals

Goals:

-   Full protobuf conformance
-   Small code size
-   Fast performance (without compromising code size)
-   Easy to wrap in language runtimes
-   Easy to adapt to different memory management schemes (refcounting, GC, etc)

Non-Goals:

-   Stable API
-   Safe API
-   Ergonomic API for applications

Parameters:

-   C99
-   32 or 64-bit CPU (assumes 4 or 8 byte pointers)
-   Uses pointer tagging, but avoids other implementation-defined behavior
-   Aims to never invoke undefined behavior (tests with ASAN, UBSAN, etc)
-   No global state, fully re-entrant

## Arenas

All memory management in upb uses arenas, using the type `upb_Arena`. Arenas are
an alternative to `malloc()` and `free()` that significantly reduces the costs
of memory allocation.

Arenas obtain blocks of memory using some underlying allocator (likely
`malloc()` and `free()`), and satisfy allocations using a simple bump allocator
that walks through each block in linear order. Allocations cannot be freed
individually: it is only possible to free the arena as a whole, which frees all
of the underlying blocks.

Here is an example of using the `upb_Arena` type:

```c
  upb_Arena* arena = upb_Arena_New();

  // Perform some allocations.
  int* x = upb_Arena_Malloc(arena, sizeof(*x));
  int* y = upb_Arena_Malloc(arena, sizeof(*y));

  // We cannot free `x` and `y` separately, we can only free the arena
  // as a whole.
  upb_Arena_Free(arena);
```

upb uses arenas for all memory management, and this fact is reflected in the API
for all upb data structures. All upb functions that allocate take a `upb_Arena*`
parameter and perform allocations using that arena rather than calling
`malloc()` or `free()`.

```c
// upb API to create a message.
UPB_API upb_Message* upb_Message_New(const upb_MiniTable* mini_table,
                                     upb_Arena* arena);

void MakeMessage(const upb_MiniTable* mini_table) {
  upb_Arena* arena = upb_Arena_New();

  // This message is allocated on our arena.
  upb_Message* msg = upb_Message_New(mini_table, arena);

  // We can free the arena whenever we want, but we cannot free the
  // message separately from the arena.
  upb_Arena_Free(arena);

  // msg is now deleted.
}
```

Arenas are a key part of upb's performance story. Parsing a large protobuf
payload usually involves rapidly creating a series of messages, arrays (repeated
fields), and maps. It is crucial for parsing performance that these allocations
are as fast as possible. Equally important, freeing the tree of messages should
be as fast as possible, and arenas can reduce this cost from `O(n)` to `O(lg
n)`.

### Avoiding Dangling Pointers

Objects allocated on an arena will frequently contain pointers to other
arena-allocated objects. For example, a `upb_Message` will have pointers to
sub-messages that are also arena-allocated.

Unlike unique ownership schemes (such as `unique_ptr<>`), arenas cannot provide
automatic safety from dangling pointers. Instead, upb provides tools to help
bridge between higher-level memory management schemes (GC, refcounting, RAII,
borrow checkers) and arenas.

If there is only one arena, dangling pointers within the arena are impossible,
because all objects are freed at the same time. This is the simplest case. The
user must still be careful not to keep dangling pointers that point at arena
memory after it has been freed, but dangling pointers *between* the arena
objects will be impossible.

But what if there are multiple arenas? If we have a pointer from one arena to
another, how do we ensure that this will not become a dangling pointer?

To help with the multiple arena case, upb provides a primitive called "fuse".

```c
// Fuses the lifetimes of `a` and `b`.  None of the blocks from `a` or `b`
// will be freed until both arenas are freed.
UPB_API bool upb_Arena_Fuse(const upb_Arena* a, const upb_Arena* b);
```

When two arenas are fused together, their lifetimes are irreversibly joined,
such that none of the arena blocks in either arena will be freed until *both*
arenas are freed with `upb_Arena_Free()`. This means that dangling pointers
between the two arenas will no longer be possible.

Fuse is useful when joining two messages from separate arenas (making one a
sub-message of the other). Fuse is a relatively cheap operation, on the order of
150ns, and is very nearly `O(1)` in the number of arenas being fused (the true
complexity is the inverse Ackermann function, which grows extremely slowly).

Each arena does consume some memory, so repeatedly creating and fusing an
additional arena is not free, but the CPU cost of fusing two arenas together is
modest.

### Initial Block and Custom Allocators

`upb_Arena` normally uses `malloc()` and `free()` to allocate and return its
underlying blocks. But this default strategy can be customized to support the
needs of a particular language.

The lowest-level function for creating a `upb_Arena` is:

```c
// Creates an arena from the given initial block (if any -- n may be 0).
// Additional blocks will be allocated from |alloc|.  If |alloc| is NULL,
// this is a fixed-size arena and cannot grow.
UPB_API upb_Arena* upb_Arena_Init(void* mem, size_t n, upb_alloc* alloc);
```

The buffer `[mem, n]` will be used as an "initial block", which is used to
satisfy allocations before calling any underlying allocation function. Note that
the `upb_Arena` itself will be allocated from the initial block if possible, so
the amount of memory available for allocation from the arena will be less than
`n`.

The `alloc` parameter specifies a custom memory allocation function which will
be used once the initial block is exhausted. The user can pass `NULL` as the
allocation function, in which case the initial block is the only memory
available in the arena. This can allow upb to be used even in situations where
there is no heap.

It follows that `upb_Arena_Malloc()` is a fallible operation, and all allocating
operations like `upb_Message_New()` should be checked for failure if there is
any possibility that a fixed size arena is in use.

## Schemas

Nearly all operations in upb require that you have a schema. A protobuf schema
is a data structure that contains all of the message, field, enum, etc.
definitions that are specified in a `.proto` file. To create, parse, serialize,
or access a message you must have a schema. For this reason, loading a schema is
generally the first thing you must do when you use upb. [^0]

[^0]: This requirement comes from the protobuf wire format itself, which is a
    deep insight about the nature of protobuf (or at least the existing wire
    format). Unlike JSON, protobuf cannot be parsed or manipulated in a
    schema-less way. This is because the binary wire format does not
    distinguish between strings and sub-messages, so a generic parser that is
    oblivious to the schema is not possible. If a future version of the wire
    format did distinguish between these, it could be possible to have a
    schema-agnostic data representation, parser, and serializer.

upb has two main data structures that represent a protobuf schema:

*   **MiniTables** are a compact, stripped down version of the schema that
    contains only the information necessary for parsing and serializing the
    binary wire format.
*   **Reflection** contains basically all of the data from a `.proto` file,
    including the original names of all messages/fields/etc., and all options.

The table below summarizes the main differences between these two:

|                     | MiniTables                | Reflection                 |
| ------------------- | ------------------------- | -------------------------- |
| Contains            | Field numbers and types   | All data in `.proto` file, |
:                     : only                      : including names of         :
:                     :                           : everything                 :
| Used to parse       | binary format             | JSON / TextFormat          |
| Wire representation | MiniDescriptor            | Descriptor                 |
| Type names          | `upb_MiniTable`,          | `upb_MessageDef`,          |
:                     : `upb_MiniTableField`, ... : `upb_FieldDef`, ...        :
| Registry            | `upb_ExtensionRegistry`   | `upb_DefPool`              |
:                     : (for extensions)          :                            :

MiniTables are useful if you only need the binary wire format, because they are
much lighter weight than full reflection.

Reflection is useful if you need to parse JSON or TextFormat, or you need access
to options that were specified in the `proto` file. Note that reflection also
includes MiniTables, so if you have reflection, you also have MiniTables
available.

### MiniTables

MiniTables are represented by a set of data structures with names like
`upb_MiniTable` (representing a message), `upb_MiniTableField`,
`upb_MiniTableFile`, etc. Whenever you see one of these types in a function
signature, you know that this particular operation requires a MiniTable. For
example:

```
// Parses the wire format data in the given buffer `[buf, size]` and writes it
// to the message `msg`, which has the type `mt`.
UPB_API upb_DecodeStatus upb_Decode(const char* buf, size_t size,
                                    upb_Message* msg, const upb_MiniTable* mt,
                                    const upb_ExtensionRegistry* extreg,
                                    int options, upb_Arena* arena);
```

The subset of upb that requires only MiniTables can be thought of as "upb lite,"
because both the code size and the runtime memory overhead will be less than
"upb full" (the parts that use reflection).

#### Loading

There are three main ways of loading a MiniTable:

1.  **From C generated code:** The upb code generator can emit
    `.upb_minitable.c` files that contain the MiniTables as global constant
    variables. When the main program links against these, the MiniTable will be
    placed into `.rodata` (or `.data.rel.ro`) in the binary. The MiniTable can
    then be obtained from a generated function. In Blaze/Bazel these files can
    be generated and linked using the `upb_minitable_proto_library()` rule.
2.  **From MiniDescriptors:** The user can build MiniDescriptors into MiniTables
    at runtime. MiniDescriptors are a compact upb-specific wire format designed
    specially for this purpose. The user can call `upb_MiniTable_Build()` at
    runtime to convert MiniDescriptors to MiniTables.
3.  **From reflection:** If you have already built reflection data structures
    for your type, then you can obtain the `upb_MiniTable` corresponding to a
    `upb_MessageDef` using `upb_MessageDef_MiniTable()`.

For languages that are already using reflection, (3) is an obvious choice.

For languages that are avoiding reflection, here is a general guideline for
choosing between (1) and (2): if the language being wrapped participates in the
standard binary linking model on a given platform (in particular, if it is
generally linked using `ld`), then it is better to use (1), which is also known
as "static loading".

Static loading of MiniTables has the benefit of requiring no runtime
initialization[^2], leading to faster startup. Static loading of MiniTables also
facilitates cross-language sharing of proto messages, because sharing generally
requires that both languages are using exactly the same MiniTables.

The main downside of static loading is that it requires the user to generate one
`.upb.c` file per `.proto` and link against the transitive closure of `.upb.c`
files. Blaze and Bazel make this reasonably easy, but for other build systems it
can be more of a challenge.

[^2]: aside from possible pointer relocations performed by the ELF/Mach-O loader
    if the library or binary is position-independent

Loading from MiniDescriptors, as in option (2), has the advantage that it does
not require per-message linking of C code. For many language toolchains,
generating and linking some custom C code for every protobuf file or message
type would be a burdensome requirement. MiniDescriptors are a convenient way of
loading MiniTables without needing to cross the FFI boundary outside the core
runtime.

A common pattern when using dynamic loading is to embed strings containing
MiniDescriptors directly into generated code. For example, the generated code in
Dart for a message with only primitive fields currently looks something like:

```dart
  const desc = r'$(+),*-#$%&! /10';
  _accessor = $pb.instance.registry.newMessageAccessor(desc);
```

The implementation of `newMessageAccessor()` is mainly just a wrapper around
`upb_MiniTable_Build()`, which builds a MiniTable from a MiniDescriptor. In the
code generator, the MiniDescriptor can be obtained from the
`upb_MessageDef_MiniDescriptorEncode()` API; users should never need to encode a
MiniDescriptor manually.

#### Linking

When building MiniTables dynamically, it is the user's responsibility to link
each message to the to the appropriate sub-messages and or enums. Each message
must have its message and closed enum fields linked using
`upb_MiniTable_SetSubMessage()` and `upb_MiniTable_SetSubEnum()`, respectively.

A higher-level function that links all fields at the same time is also
available, as `upb_MiniTable_Link()`. This function pairs well with
`upb_MiniTable_GetSubList()` which can be used in a code generator to get a list
of all the messages and enums which must be passed to `upb_MiniTable_Link()`.

A common pattern is to embed the `link()` calls directly into the generated
code. For example, here is an example from Dart of building a MiniTable that
contains sub-messages and enums:

```dart
  const desc = r'$3334';
  _accessor = $pb.instance.registry.newMessageAccessor(desc);
  _accessor!.link(
      [
        M2.$_accessor,
        M3.$_accessor,
        M4.$_accessor,
      ],
      [
        E.$_accessor,
      ],
  );
```

In this case, `upb_MiniTable_GetSubList()` was used in the code generator to
discover the 3 sub-message fields and 1 sub-enum field that require linking. At
runtime, these lists of MiniTables are passed to the `link()` function, which
will internally call `upb_MiniTable_Link()`.

Note that in some cases, the application may choose to delay or even skip the
registration of sub-message types, as part of a tree shaking strategy.

When using static MiniTables, a manual link step is not necessary, as linking is
performed automatically by `ld`.

#### Enums

MiniTables primarily carry data about messages, fields, and extensions. However
for closed enums, we must also have a `upb_MiniTableEnum` structure that stores
the set of all numbers that are defined in the enum. This is because closed
enums have the unfortunate behavior of putting unknown enum values into the
unknown field set.

Over time, closed enums will hopefully be phased out via editions, and the
relevance and overhead of `upb_MiniTableEnum` will shrink and eventually
disappear.

### Reflection

Reflection uses types like `upb_MessageDef` and `upb_FieldDef` to represent the
full contents of a `.proto` file at runtime. These types are upb's direct
equivalents of `google::protobuf::Descriptor`, `google::protobuf::FieldDescriptor`, etc. [^1]

[^1]: upb consistently uses `Def` where C++ would use `Descriptor` in type
    names. This introduces divergence with C++; the rationale was to conserve
    horizontal line length, as `Def` is less than 1/3 the length of
    `Descriptor`. This is more relevant in C, where the type name is repeated
    in every function, eg. `upb_FieldDef_Name()` vs.
    `upb_FieldDescriptor_Name()`.

Whenever you see one of these types in a function signature, you know that the
given operation requires reflection. For example:

```c
// Parses JSON format into a message object, using reflection.
UPB_API bool upb_JsonDecode(const char* buf, size_t size, upb_Message* msg,
                            const upb_MessageDef* m, const upb_DefPool* symtab,
                            int options, upb_Arena* arena, upb_Status* status);
```

The part of upb that requires reflection can be thought of as "upb full." These
parts of the library cannot be used if a given application has only loaded
MiniTables. There is no way to convert a MiniTable into reflection.

The `upb_DefPool` type is the top-level container that builds and owns some set
of defs. This type is a close analogue of `google::protobuf::DescriptorPool` in C++. The
user must always ensure that the `upb_DefPool` outlives any def objects that it
owns.

#### Loading

As with MiniTable loading, we have multiple options for how to load full
reflection:

1.  **From C generated code**: The upb code generator can create `foo.upbdefs.c`
    files that embed the descriptors and exports generated C functions for
    adding them to a user-provided `upb_DefPool`.
2.  **From descriptors**: The user can make manual calls to
    `upb_DefPool_AddFile()`, using descriptors obtained at runtime. Defs for
    individual messages can then be obtained using
    `upb_DefPool_FindMessageByName()`.

Unlike MiniTables, loading from generated code requires runtime initialization,
as reflection data structures like `upb_MessageDef` are not capable of being
emitted directly into `.rodata` like `upb_MiniTable` is. Instead, the generated
code embeds serialized descriptor protos into `.rodata` which are then built
into heap objects at runtime.

From this you might conclude that option (1) is nothing but a convenience
wrapper around option (2), but that is not quite correct either. Option (1)
*does* link against the static `.upb.c` structures for the MiniTables, whereas
option (2) will build the MiniTables from scratch on the heap. So option (1)
will use marginally less CPU and RAM when the descriptors are loaded into a
`upb_DefPool`. More importantly, the resulting descriptors will be capable of
reflecting over any messages built from the generated `.upb.c` MiniTables,
whereas descriptors built using option (2) will have distinct MiniTables that
cannot reflect over messages that use the generated MiniTables.

A common pattern for dynamic languages like PHP, Ruby, or Python, is to use
option (2) with descriptors that are embedded into the generated code. For
example, the generated code in Python currently looks something like:

```python
from google.protobuf import descriptor_pool as _descriptor_pool
from google.protobuf.internal import builder as _builder

_desc = b'\n\x1aprotoc_explorer/main.proto\x12\x03pkg'

DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(_desc)
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google3.protoc_explorer.main_pb2', _globals)
```

The `AddSerializedFile()` API above is mainly just a thin wrapper around
`upb_DefPool_AddFile()`.