Skip to content

Conversation

allevato
Copy link
Collaborator

In order to perform the low-level typed operations on the raw memory, the protobuf runtime needs to be able to refer statically to the type of a submessage field. This is inherently challenging when we're trying to use a single storage class to represent all possible messages.

To achieve this, we generate trampoline functions that are passed into the _MessageLayout initializer and stored. Then, for example, when deinitializing a submessage field, the storage class calls the deinitializer trampoline with the appropriate submessage index and the trampoline function immediately calls back into the storage class but passes it the type hint so that the memory can be bound to the correct type. Likewise for copying a submessage field.

This replaces the original notion of a submessage accessor function that relied on returning the metatype as an existential value. That wouldn't be compatible with embedded Swift; these type hints should be.

I've also updated the generator to avoid generating these trampoline functions if a message doesn't have any submessages. In this case, we can just pass a placeholder function baked into the runtime that does a precondition failure, so we save a bit on codegen.

In order to perform the low-level typed operations on the raw
memory, the protobuf runtime needs to be able to refer statically
to the type of a submessage field. This is inherently challenging
when we're trying to use a single storage class to represent all
possible messages.

To achieve this, we generate trampoline functions that are passed
into the `_MessageLayout` initializer and stored. Then, for example,
when deinitializing a submessage field, the storage class calls the
deinitializer trampoline with the appropriate submessage index and
the trampoline function immediately calls back into the storage
class but passes it the type hint so that the memory can be bound
to the correct type. Likewise for copying a submessage field.

This replaces the original notion of a submessage accessor function
that relied on returning the metatype as an existential value. That
wouldn't be compatible with embedded Swift; these type hints should
be.

I've also updated the generator to avoid generating these trampoline
functions if a message doesn't have any submessages. In this case,
we can just pass a placeholder function baked into the runtime that
does a precondition failure, so we save a bit on codegen.
The new tests verify submessages (singular and repeated),
including CoW behavior.
@allevato allevato added the semver/none No version bump required. label Oct 14, 2025
static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}optional_int32\0\u{3}optional_int64\0\u{3}optional_uint32\0\u{3}optional_uint64\0\u{3}optional_sint32\0\u{3}optional_sint64\0\u{3}optional_fixed32\0\u{3}optional_fixed64\0\u{3}optional_sfixed32\0\u{3}optional_sfixed64\0\u{3}optional_float\0\u{3}optional_double\0\u{3}optional_bool\0\u{3}optional_string\0\u{3}optional_bytes\0\u{7}OptionalGroup\0\u{4}\u{2}optional_nested_message\0\u{3}optional_foreign_message\0\u{3}optional_import_message\0\u{3}optional_nested_enum\0\u{3}optional_foreign_enum\0\u{3}optional_import_enum\0\u{4}\u{3}optional_public_import_message\0\u{4}\u{5}repeated_int32\0\u{3}repeated_int64\0\u{3}repeated_uint32\0\u{3}repeated_uint64\0\u{3}repeated_sint32\0\u{3}repeated_sint64\0\u{3}repeated_fixed32\0\u{3}repeated_fixed64\0\u{3}repeated_sfixed32\0\u{3}repeated_sfixed64\0\u{3}repeated_float\0\u{3}repeated_double\0\u{3}repeated_bool\0\u{3}repeated_string\0\u{3}repeated_bytes\0\u{7}RepeatedGroup\0\u{4}\u{2}repeated_nested_message\0\u{3}repeated_foreign_message\0\u{3}repeated_import_message\0\u{3}repeated_nested_enum\0\u{3}repeated_foreign_enum\0\u{3}repeated_import_enum\0\u{4}\u{8}default_int32\0\u{3}default_int64\0\u{3}default_uint32\0\u{3}default_uint64\0\u{3}default_sint32\0\u{3}default_sint64\0\u{3}default_fixed32\0\u{3}default_fixed64\0\u{3}default_sfixed32\0\u{3}default_sfixed64\0\u{3}default_float\0\u{3}default_double\0\u{3}default_bool\0\u{3}default_string\0\u{3}default_bytes\0\u{4}\u{6}default_nested_enum\0\u{3}default_foreign_enum\0\u{3}default_import_enum\0\u{4}\u{1c}oneof_uint32\0\u{3}oneof_nested_message\0\u{3}oneof_string\0\u{3}oneof_bytes\0\u{b}something_old\0\u{b}reserved_field\0\u{b}something_long_gone\0\u{c}JIt\u{3}\u{1}\u{c}LIt\u{3}\u{1}\u{c}lHt\u{3}\u{a}\u{c}~Ht\u{3}\u{2}\u{c}KIt\u{3}\u{1}")
#if _pointerBitWidth(_64)
static let _protobuf_messageLayout = SwiftProtobuf._MessageLayout("\0\u{10}\u{4}\0C\0\0\0\0\0\u{11}\0\0\u{1}\0\0\0\0 \0\0\0\0\0\0\u{5}\u{2}\0\0\0\0p\0\0\u{1}\0\0\0\u{3}\u{3}\0\0\0\0$\0\0\u{2}\0\0\0\u{d}\u{4}\0\0\0\0x\0\0\u{3}\0\0\0\u{4}\u{5}\0\0\0\0(\0\0\u{4}\0\0\0\u{11}\u{6}\0\0\0\0\0\u{1}\0\u{5}\0\0\0\u{12}\u{7}\0\0\0\0,\0\0\u{6}\0\0\0\u{7}\u{8}\0\0\0\0\u{8}\u{1}\0\u{7}\0\0\0\u{6}\u{9}\0\0\0\00\0\0\u{8}\0\0\0\u{f}\u{a}\0\0\0\0\u{10}\u{1}\0\u{9}\0\0\0\u{10}\u{b}\0\0\0\04\0\0\u{a}\0\0\0\u{2}\u{c}\0\0\0\0\u{18}\u{1}\0\u{b}\0\0\0\u{1}\u{d}\0\0\0\0\u{1c}\0\0\u{c}\0\0\0\u{8}\u{e}\0\0\0\00\u{3}\0\u{d}\0\0\0\u{9}\u{f}\0\0\0\0@\u{3}\0\u{e}\0\0\0\u{c}\u{10}\0\0\0\0P\u{1}\0\u{f}\0\0\0\u{a}\u{12}\0\0\0\0X\u{1}\0\u{10}\0\0\0\u{b}\u{13}\0\0\0\0`\u{1}\0\u{11}\0\0\0\u{b}\u{14}\0\0\0\0h\u{1}\0\u{12}\0\0\0\u{b}\u{15}\0\0\0\08\0\0\u{13}\0\0\0\u{e}\u{16}\0\0\0\0<\0\0\u{14}\0\0\0\u{e}\u{17}\0\0\0\0@\0\0\u{15}\0\0\0\u{e}\u{1a}\0\0\0\0p\u{1}\0\u{16}\0\0\0\u{b}\u{1f}\0\0\0\u{2}x\u{1}\0\u{17}\0\0\0\u{5} \0\0\0\u{2}\0\u{2}\0\u{18}\0\0\0\u{3}!\0\0\0\u{2}\u{8}\u{2}\0\u{19}\0\0\0\u{d}\"\0\0\0\u{2}\u{10}\u{2}\0\u{1a}\0\0\0\u{4}#\0\0\0\u{2}\u{18}\u{2}\0\u{1b}\0\0\0\u{11}$\0\0\0\u{2} \u{2}\0\u{1c}\0\0\0\u{12}%\0\0\0\u{2}(\u{2}\0\u{1d}\0\0\0\u{7}&\0\0\0\u{2}0\u{2}\0\u{1e}\0\0\0\u{6}'\0\0\0\u{2}8\u{2}\0\u{1f}\0\0\0\u{f}(\0\0\0\u{2}@\u{2}\0 \0\0\0\u{10})\0\0\0\u{2}H\u{2}\0!\0\0\0\u{2}*\0\0\0\u{2}P\u{2}\0\"\0\0\0\u{1}+\0\0\0\u{2}X\u{2}\0#\0\0\0\u{8},\0\0\0\u{2}`\u{2}\0$\0\0\0\u{9}-\0\0\0\u{2}h\u{2}\0%\0\0\0\u{c}.\0\0\0\u{2}p\u{2}\0&\0\0\0\u{a}0\0\0\0\u{2}x\u{2}\0'\0\0\0\u{b}1\0\0\0\u{2}\0\u{3}\0(\0\0\0\u{b}2\0\0\0\u{2}\u{8}\u{3}\0)\0\0\0\u{b}3\0\0\0\u{2}\u{10}\u{3}\0*\0\0\0\u{e}4\0\0\0\u{2}\u{18}\u{3}\0+\0\0\0\u{e}5\0\0\0\u{2} \u{3}\0,\0\0\0\u{e}=\0\0\0\0D\0\0-\0\0\0\u{5}>\0\0\0\0 \u{1}\0.\0\0\0\u{3}?\0\0\0\0H\0\0/\0\0\0\u{d}@\0\0\0\0(\u{1}\00\0\0\0\u{4}A\0\0\0\0L\0\01\0\0\0\u{11}B\0\0\0\00\u{1}\02\0\0\0\u{12}C\0\0\0\0P\0\03\0\0\0\u{7}D\0\0\0\08\u{1}\04\0\0\0\u{6}E\0\0\0\0T\0\05\0\0\0\u{f}F\0\0\0\0@\u{1}\06\0\0\0\u{10}G\0\0\0\0X\0\07\0\0\0\u{2}H\0\0\0\0H\u{1}\08\0\0\0\u{1}I\0\0\0\0\u{1d}\0\09\0\0\0\u{8}J\0\0\0\0P\u{3}\0:\0\0\0\u{9}K\0\0\0\0`\u{3}\0;\0\0\0\u{c}Q\0\0\0\0\\\0\0<\0\0\0\u{e}R\0\0\0\0`\0\0=\0\0\0\u{e}S\0\0\0\0d\0\0>\0\0\0\u{e}o\0\0\0\0h\0\0s\u{7f}\0\0\u{d}p\0\0\0\0(\u{3}\0s\u{7f}\0\0\u{b}q\0\0\0\0p\u{3}\0s\u{7f}\0\0\u{9}r\0\0\0\0\0\u{4}\0s\u{7f}\0\0\u{c}")
@_alwaysEmitIntoClient @inline(__always)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why the directives here since this is already in the generated code? Does it force it into the init call?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So as part of my deep diving into codegen, if this was just a regular property, we'd still generate a separate accessor for it. That accessor serves no purpose, because the string is only referenced from one place. @_alwaysEmitIntoClient ensures that the symbol for the entry point is never generated.

Now that the _MessageLayout initializer can take other functions, I didn't want to duplicate all of that in each branch of the #if. So this change lets us break out the layout string into a separate property and make that be the only part that's conditionally compiled, and then the _MessageLayout initializer just references the symbol at no cost because it will be emitted directly at the call site.

let submessageNames = messageLayoutCalculator.submessageNames
let deinitializeSubmessageName: String
let copySubmessageName: String
if submessageNames.isEmpty {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if it would save anything, but you could put two initializers, one with hooks and one without, and then have the runtime carry the one without the hooks that just defaults the arguments. it might even let you make those default private within the runtime so theres a few less symbols to expose.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. That makes the generator a lot cleaner and it also means we don't have to expose those entry points outside of the runtime.

@allevato
Copy link
Collaborator Author

Merging this since I have oneofs coming up, stacked on top of this.

@allevato allevato merged commit e944df2 into apple:table-driven-branch Oct 15, 2025
1 check passed
@allevato allevato deleted the table-driven-submessages branch October 15, 2025 16:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

semver/none No version bump required.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants