Internal change PiperOrigin-RevId: 999512046
diff --git a/objectivec/GPBDescriptor.m b/objectivec/GPBDescriptor.m index 9c41d51..df6d666 100644 --- a/objectivec/GPBDescriptor.m +++ b/objectivec/GPBDescriptor.m
@@ -9,6 +9,7 @@ #import "GPBDescriptor_PackagePrivate.h" #import <objc/runtime.h> +#import <stdatomic.h> #import "GPBBootstrap.h" #import "GPBMessage.h" @@ -1227,4 +1228,29 @@ @end +// See GPBDescriptor_PackagePrivate.h for full details on section-based +// extension registration and atomic descriptor resolution. +GPBExtensionDescriptor *GPBResolveExtensionSectionEntry(GPBExtensionSectionEntry *entry, + const int32_t *runtimeSupport) { + _Atomic(GPBExtensionDescriptor *) *descriptorPtr = + (_Atomic(GPBExtensionDescriptor *) *)&entry->descriptor; + GPBExtensionDescriptor *descriptor = atomic_load(descriptorPtr); + if (descriptor) { + return descriptor; + } + + GPBExtensionDescriptor *expected = nil; + GPBExtensionDescriptor *created = + [[GPBExtensionDescriptor alloc] initWithExtensionDescription:entry->description + runtimeSupport:runtimeSupport]; + if (atomic_compare_exchange_strong(descriptorPtr, &expected, created)) { + // Value was set, return it. + return created; + } + + // Some other thread set it, release the one created and return what got set. + [created release]; + return expected; +} + #pragma clang diagnostic pop
diff --git a/objectivec/GPBDescriptor_PackagePrivate.h b/objectivec/GPBDescriptor_PackagePrivate.h index ed68530..e85e198 100644 --- a/objectivec/GPBDescriptor_PackagePrivate.h +++ b/objectivec/GPBDescriptor_PackagePrivate.h
@@ -324,6 +324,85 @@ return (description->options & GPBExtensionPacked) != 0; } +// Mach-O section-based extension registration (inspired by upb's linker-array +// UPB_LINKARR design). +// +// When generated with the `experimental_global_extension_registry` generator +// option (`extension_generation_mode=c_function`), each generated C-function +// extension descriptor accessor places a two-pointer GPBExtensionSectionEntry +// struct (`{description, descriptor}`) into the custom Mach-O section in the +// "__DATA" segment, and delegates descriptor creation to +// GPBResolveExtensionSectionEntry(). +// +// Interaction with linker dead-stripping (-dead_strip): +// - When an object file (.o) from a static archive (.a) is NOT pulled into the +// final binary by the linker (because neither the extension C function nor +// any co-located message class in that object file is referenced), the +// entire object file—including its GPBExtensionSectionEntry—is omitted. +// - When an object file IS pulled into the binary, __attribute__((used)) +// retains the GPBExtensionSectionEntry within that translation unit, and +// pointer-sized alignment ensures the linker packs all surviving entries into +// a contiguous array in the section with zero inter-element padding. +// +// Lazy indexing & descriptor resolution: +// - Calling +[GPBExtensionRegistry globalExtensionRegistry] only allocates the +// lightweight registry wrapper. Its first -extensionForDescriptor:fieldNumber: +// call registers a dyld add-image callback +// (_dyld_register_func_for_add_image), which synchronously indexes the +// section of every already-loaded Mach-O image via getsectiondata() (and any +// images dynamically loaded later) into a map of +// (extended Class -> fieldNumber -> GPBExtensionSectionEntry *). +// - Indexing only records raw GPBExtensionSectionEntry pointers without +// allocating GPBExtensionDescriptor objects or sending Objective-C messages. +// - A GPBExtensionDescriptor is lazily allocated on first use by +// GPBResolveExtensionSectionEntry() via an atomic compare-and-exchange on +// `entry->descriptor`. Both direct calls to the generated C extension +// function and dynamic lookups through +[GPBExtensionRegistry +// globalExtensionRegistry] funnel through GPBResolveExtensionSectionEntry(), +// eliminating per-extension `dispatch_once_t` storage and block descriptors +// from generated `.pbobjc.m` files while sharing a single cached descriptor +// instance. +#define GPB_EXTENSION_SECTION_SEGMENT "__DATA" + +// SDKs that embed a copy of the runtime may override GPB_EXTENSION_SECTION_NAME +// to isolate their extension section from other copies linked into the same +// process. +#if !defined(GPB_EXTENSION_SECTION_NAME) +#define GPB_EXTENSION_SECTION_NAME "__gpb_objc_ext" +#endif + +#define GPB_EXTENSION_REGISTRY_SECTION_ATTR \ + __attribute__((used, aligned(sizeof(void *)), \ + section(GPB_EXTENSION_SECTION_SEGMENT "," GPB_EXTENSION_SECTION_NAME))) + +// A single entry in the extension registration Mach-O section. +// - `description`: Static pointer to the extension's compile-time +// GPBExtensionDescription metadata (extended class, payload class, field +// number, data type, options). +// - `descriptor`: Lazily initialized GPBExtensionDescriptor singleton, +// populated atomically on first access via GPBResolveExtensionSectionEntry(). +// Marked GPB_UNSAFE_UNRETAINED so this struct can live in a static C section +// even when this header is included under ARC; +// GPBResolveExtensionSectionEntry() allocates the descriptor with +alloc/-init +// (+1 retain count) and intentionally keeps that reference alive for the +// lifetime of the process when the atomic compare-and-exchange succeeds (or +// releases the contender if another thread won the race). +typedef struct GPBExtensionSectionEntry { + GPBExtensionDescription *description; + GPB_UNSAFE_UNRETAINED GPBExtensionDescriptor *descriptor; +} GPBExtensionSectionEntry; + +// Lazily allocates and caches the GPBExtensionDescriptor instance for `entry` +// in a thread-safe manner (see block comment above for full details). +GPBExtensionDescriptor *GPBResolveExtensionSectionEntry(GPBExtensionSectionEntry *entry, + const int32_t *runtimeSupport); + +// Returns YES once the registry returned by +// +[GPBExtensionRegistry globalExtensionRegistry] has started reading the +// extension sections of the loaded images, which happens on its first +// -extensionForDescriptor:fieldNumber: call. Exposed for unit tests. +BOOL GPBGlobalExtensionRegistryHasStartedIndexing(void); + // Helper for compile time assets. #ifndef GPBInternalCompileAssert #define GPBInternalCompileAssert(test, msg) _Static_assert((test), #msg) @@ -335,6 +414,17 @@ (sizeof(GPBGenericValue) + sizeof(GPBMessageFieldDescription)), DescriptionsWithDefault_different_size_than_expected); +// Ensure GPBExtensionSectionEntry has no internal or trailing padding so that +// contiguous section entries can be indexed as a flat C array by +// getsectiondata(). +GPBInternalCompileAssert(sizeof(GPBExtensionSectionEntry) == 2 * sizeof(void *), + ExtensionSectionEntry_different_size_than_expected); + +// Mach-O section names are limited to 16 characters (plus the NUL terminator +// in the C string literal). +GPBInternalCompileAssert(sizeof(GPB_EXTENSION_SECTION_NAME) <= 17, + ExtensionSectionName_exceeds_macho_16_char_limit); + // Sanity check that the file description structures old and new work. GPBInternalCompileAssert(offsetof(GPBFilePackageAndPrefix, package) == offsetof(GPBFileDescription, package),
diff --git a/objectivec/GPBExtensionRegistry.h b/objectivec/GPBExtensionRegistry.h index 9ca8ae5..7a1d2a3 100644 --- a/objectivec/GPBExtensionRegistry.h +++ b/objectivec/GPBExtensionRegistry.h
@@ -65,6 +65,18 @@ **/ - (void)addExtensions:(GPBExtensionRegistry *)registry; +/** + * Returns the process-wide registry of the extensions that are linked into the + * process and were generated with the experimental + * `experimental_global_extension_registry` generator option (C function + * extensions only). + * + * The returned object is immutable and can be used from any thread. It is not + * a `GPBExtensionRegistry` instance, but it can be passed to any API that takes + * an `id<GPBExtensionRegistry>`. + **/ ++ (id<GPBExtensionRegistry>)globalExtensionRegistry; + @end NS_ASSUME_NONNULL_END
diff --git a/objectivec/GPBExtensionRegistry.m b/objectivec/GPBExtensionRegistry.m index d460def..6f6b6ba 100644 --- a/objectivec/GPBExtensionRegistry.m +++ b/objectivec/GPBExtensionRegistry.m
@@ -7,8 +7,168 @@ #import "GPBExtensionRegistry.h" +#import <mach-o/dyld.h> +#import <mach-o/getsect.h> +#import <stdatomic.h> +#import <stdlib.h> + #import "GPBBootstrap.h" #import "GPBDescriptor.h" +#import "GPBDescriptor_PackagePrivate.h" + +#pragma mark - Global extension registry + +// Pointer-width-appropriate Mach-O header type for getsectiondata(). +#if __LP64__ +typedef struct mach_header_64 GPBMachHeader; +#else +typedef struct mach_header GPBMachHeader; +#endif + +// The index of the extension registration Mach-O section of one loaded image. +// +// `classMap` maps `Class` (extended message class) -> CFDictionaryRef of +// (ssize_t fieldNumber -> GPBExtensionSectionEntry *). Only raw pointers to the +// static section entries are stored, so building the index creates no +// GPBExtensionDescriptor objects and sends no Objective-C messages. +// +// An index is fully built before it is published and is never modified or +// freed after that (images with Objective-C classes are never unloaded), so +// lookups read published indexes without taking a lock. +typedef struct GPBExtensionSectionImageIndex { + CFDictionaryRef classMap; + struct GPBExtensionSectionImageIndex *next; +} GPBExtensionSectionImageIndex; + +// Singly linked list of the published image indexes, newest first. A new +// index is pushed with a compare-and-swap (release); lookups load the head +// with acquire semantics and walk the list. +static _Atomic(GPBExtensionSectionImageIndex *) gExtensionSectionImageIndexes = NULL; + +// Set when the first lookup starts reading the sections. Only used by tests. +static atomic_bool gGlobalExtensionRegistryStartedIndexing = false; + +// Declared in GPBDescriptor_PackagePrivate.h for unit tests +// (GPBGlobalExtensionRegistryProcessTest.m). +BOOL GPBGlobalExtensionRegistryHasStartedIndexing(void) { + return atomic_load(&gGlobalExtensionRegistryStartedIndexing) ? YES : NO; +} + +// dyld add-image callback. Called synchronously for every already loaded image +// from within _dyld_register_func_for_add_image(), and later for every newly +// loaded image (on the thread loading it, before the image's initializers have +// run). Only reads the static section data. +static void GPBIndexExtensionSectionOfImage(const struct mach_header *header, + __unused intptr_t vmaddrSlide) { + unsigned long sectionSize = 0; + uint8_t *sectionData = + getsectiondata((const GPBMachHeader *)header, GPB_EXTENSION_SECTION_SEGMENT, + GPB_EXTENSION_SECTION_NAME, §ionSize); + if (sectionData == NULL || sectionSize < sizeof(GPBExtensionSectionEntry)) { + return; + } + GPBExtensionSectionEntry *entries = (GPBExtensionSectionEntry *)sectionData; + size_t entryCount = sectionSize / sizeof(GPBExtensionSectionEntry); + + CFMutableDictionaryRef classMap = + CFDictionaryCreateMutable(kCFAllocatorDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks); + for (size_t entryIndex = 0; entryIndex < entryCount; ++entryIndex) { + GPBExtensionSectionEntry *entry = &entries[entryIndex]; + // Every generated GPBExtensionSectionEntry has a non-NULL description and + // non-Nil extendedClass.clazz; assert in debug builds and guard defensively + // if assertions are disabled. + GPBExtensionDescription *description = entry->description; + NSCAssert(description != NULL, @"Internal error: section entry missing description."); + if (description == NULL) { + continue; + } + Class extendedClass = description->extendedClass.clazz; + NSCAssert(extendedClass != Nil, @"Internal error: extension %s missing extendedClass.", + description->singletonName ? description->singletonName : "<unknown>"); + if (extendedClass == Nil) { + continue; + } + CFMutableDictionaryRef fieldMap = + (CFMutableDictionaryRef)CFDictionaryGetValue(classMap, extendedClass); + if (fieldMap == NULL) { + fieldMap = CFDictionaryCreateMutable(kCFAllocatorDefault, 0, NULL, NULL); + CFDictionarySetValue(classMap, extendedClass, fieldMap); + CFRelease(fieldMap); + } + ssize_t key = description->fieldNumber; + CFDictionarySetValue(fieldMap, (const void *)key, entry); + } + if (CFDictionaryGetCount(classMap) == 0) { + CFRelease(classMap); + return; + } + + GPBExtensionSectionImageIndex *imageIndex = malloc(sizeof(GPBExtensionSectionImageIndex)); + if (imageIndex == NULL) { + abort(); + } + imageIndex->classMap = classMap; + imageIndex->next = atomic_load_explicit(&gExtensionSectionImageIndexes, memory_order_relaxed); + while (!atomic_compare_exchange_weak_explicit(&gExtensionSectionImageIndexes, &imageIndex->next, + imageIndex, memory_order_release, + memory_order_relaxed)) { + // `imageIndex->next` was updated to the current head; retry. + } +} + +// Starts indexing the extension sections the first time it is called. +static void GPBStartIndexingExtensionSections(void) { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + atomic_store(&gGlobalExtensionRegistryStartedIndexing, true); + // Per <mach-o/dyld.h>, _dyld_register_func_for_add_image() synchronously + // invokes the callback on the calling thread for every Mach-O image + // currently loaded in the process before returning, and subsequently + // invokes it as any new images are dynamically loaded (e.g. via dlopen()). + _dyld_register_func_for_add_image(&GPBIndexExtensionSectionOfImage); + }); +} + +// The immutable registry returned by +[GPBExtensionRegistry globalExtensionRegistry]. +// +// Synchronization: lookups take no lock. The first lookup indexes the already +// loaded images inside a dispatch_once, so every lookup sees them. Images +// loaded later are indexed by the dyld callback on the loading thread and +// published with a release compare-and-swap; a lookup that runs at the same +// time as an image load may not see that image's extensions yet. Descriptor +// creation goes through GPBResolveExtensionSectionEntry() (atomic +// compare-and-swap), which the generated extension functions also use. +__attribute__((objc_subclassing_restricted)) +@interface GPBGlobalSectionExtensionRegistry : NSObject<GPBExtensionRegistry> +@end + +@implementation GPBGlobalSectionExtensionRegistry + +- (GPBExtensionDescriptor *)extensionForDescriptor:(GPBDescriptor *)descriptor + fieldNumber:(NSInteger)fieldNumber { + GPBStartIndexingExtensionSections(); + Class messageClass = descriptor.messageClass; + ssize_t key = fieldNumber; + for (GPBExtensionSectionImageIndex *imageIndex = + atomic_load_explicit(&gExtensionSectionImageIndexes, memory_order_acquire); + imageIndex != NULL; imageIndex = imageIndex->next) { + CFDictionaryRef fieldMap = CFDictionaryGetValue(imageIndex->classMap, messageClass); + if (fieldMap == NULL) { + continue; + } + GPBExtensionSectionEntry *entry = + (GPBExtensionSectionEntry *)CFDictionaryGetValue(fieldMap, (const void *)key); + if (entry != NULL) { + return GPBResolveExtensionSectionEntry(entry, + &GOOGLE_PROTOBUF_OBJC_EXPECTED_GENCODE_VERSION_40311); + } + } + return nil; +} + +@end + +#pragma mark - GPBExtensionRegistry @implementation GPBExtensionRegistry { CFMutableDictionaryRef mutableClassMap_; @@ -105,6 +265,16 @@ CFDictionaryApplyFunction(registry->mutableClassMap_, CopySubDictionary, mutableClassMap_); } ++ (id<GPBExtensionRegistry>)globalExtensionRegistry { + static GPBGlobalSectionExtensionRegistry *globalRegistry = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + // Only creates the object; the sections are read on the first lookup. + globalRegistry = [[GPBGlobalSectionExtensionRegistry alloc] init]; + }); + return globalRegistry; +} + #pragma clang diagnostic pop @end
diff --git a/objectivec/Tests/GPBExtensionRegistryTest.m b/objectivec/Tests/GPBExtensionRegistryTest.m index 0c856a3..f3498dc 100644 --- a/objectivec/Tests/GPBExtensionRegistryTest.m +++ b/objectivec/Tests/GPBExtensionRegistryTest.m
@@ -5,10 +5,66 @@ // license that can be found in the LICENSE file or at // https://developers.google.com/open-source/licenses/bsd -#import "GPBExtensionRegistry.h" #import "GPBTestUtilities.h" + +#import <dispatch/dispatch.h> + +#import "GPBCodedOutputStream.h" +#import "GPBDescriptor_PackagePrivate.h" +#import "GPBExtensionRegistry.h" +#import "GPBUtilities_PackagePrivate.h" #import "objectivec/Tests/Unittest.pbobjc.h" +// Hand written "__DATA,__gpb_ext_reg" section entries, in the same form the +// generator emits with the `experimental_global_extension_registry` option. +// They make the +globalExtensionRegistry tests independent of how the test +// protos were generated. The field numbers are not used by any extension in the +// test protos, and each entry is used by one test only, so a test controls +// whether the entry's descriptor has been created yet. + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdollar-in-identifier-extension" + +GPBObjCClassDeclaration(TestAllExtensions); + +#define GPB_TEST_HAND_WRITTEN_INT32_EXTENSION(ENTRY_NAME, FIELD_NUMBER) \ + static GPBExtensionDescription ENTRY_NAME##Description = { \ + .defaultValue.valueInt32 = 0, \ + .singletonName = "GPBExtensionRegistryTest_" #ENTRY_NAME, \ + .extendedClass.clazz = GPBObjCClass(TestAllExtensions), \ + .messageOrGroupClass.clazz = Nil, \ + .enumDescriptorFunc = NULL, \ + .fieldNumber = FIELD_NUMBER, \ + .dataType = GPBDataTypeInt32, \ + .options = GPBExtensionNone, \ + }; \ + GPB_EXTENSION_REGISTRY_SECTION_ATTR static GPBExtensionSectionEntry ENTRY_NAME = { \ + .description = &ENTRY_NAME##Description, \ + .descriptor = nil, \ + } + +// Enum constants, so they can be used in the static initializers. +enum { + kLookupExtensionFieldNumber = 30001, + kConcurrentExtensionFieldNumber = 30002, + kParseExtensionFieldNumber = 30003, + kUnusedExtensionFieldNumber = 30099, +}; + +GPB_TEST_HAND_WRITTEN_INT32_EXTENSION(gLookupExtensionEntry, kLookupExtensionFieldNumber); +GPB_TEST_HAND_WRITTEN_INT32_EXTENSION(gConcurrentExtensionEntry, kConcurrentExtensionFieldNumber); +GPB_TEST_HAND_WRITTEN_INT32_EXTENSION(gParseExtensionEntry, kParseExtensionFieldNumber); + +#undef GPB_TEST_HAND_WRITTEN_INT32_EXTENSION + +#pragma clang diagnostic pop + +// What a generated extension function does: resolve the entry's descriptor. +static GPBExtensionDescriptor *ResolveHandWrittenEntry(GPBExtensionSectionEntry *entry) { + return GPBResolveExtensionSectionEntry(entry, + &GOOGLE_PROTOBUF_OBJC_EXPECTED_GENCODE_VERSION_40311); +} + @interface GPBExtensionRegistryTest : GPBTestCase @end @@ -183,4 +239,161 @@ #endif } +#pragma mark - +globalExtensionRegistry + +- (void)testGlobalExtensionRegistryIsSingleton { + id<GPBExtensionRegistry> globalRegistry = [GPBExtensionRegistry globalExtensionRegistry]; + XCTAssertNotNil(globalRegistry); + XCTAssertTrue([GPBExtensionRegistry globalExtensionRegistry] == globalRegistry); // ptr equality + XCTAssertTrue([globalRegistry conformsToProtocol:@protocol(GPBExtensionRegistry)]); + // Immutable, so it is not a (mutable) GPBExtensionRegistry. + XCTAssertFalse([(NSObject *)globalRegistry isKindOfClass:[GPBExtensionRegistry class]]); + + const size_t kIterationCount = 256; + id<GPBExtensionRegistry> *concurrentResults = + calloc(kIterationCount, sizeof(id<GPBExtensionRegistry>)); + dispatch_apply(kIterationCount, dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), + ^(size_t iteration) { + concurrentResults[iteration] = [GPBExtensionRegistry globalExtensionRegistry]; + }); + for (size_t iteration = 0; iteration < kIterationCount; ++iteration) { + XCTAssertTrue(concurrentResults[iteration] == globalRegistry); // ptr equality + } + free(concurrentResults); +} + +- (void)testGlobalExtensionRegistryReturnsSameDescriptorAsSectionEntry { + id<GPBExtensionRegistry> globalRegistry = [GPBExtensionRegistry globalExtensionRegistry]; + XCTAssertTrue(gLookupExtensionEntry.descriptor == nil); + + GPBExtensionDescriptor *fromRegistry = + [globalRegistry extensionForDescriptor:[TestAllExtensions descriptor] + fieldNumber:kLookupExtensionFieldNumber]; + XCTAssertNotNil(fromRegistry); + XCTAssertTrue(gLookupExtensionEntry.descriptor == fromRegistry); // ptr equality + XCTAssertEqual(fromRegistry.fieldNumber, (uint32_t)kLookupExtensionFieldNumber); + XCTAssertTrue(fromRegistry.containingMessageClass == [TestAllExtensions class]); + XCTAssertEqual(fromRegistry.dataType, GPBDataTypeInt32); + + // The generated function path returns the same instance. + XCTAssertTrue(ResolveHandWrittenEntry(&gLookupExtensionEntry) == fromRegistry); // ptr equality + // And repeated lookups do too. + XCTAssertTrue([globalRegistry extensionForDescriptor:[TestAllExtensions descriptor] + fieldNumber:kLookupExtensionFieldNumber] == + fromRegistry); // ptr equality +} + +- (void)testGlobalExtensionRegistryReturnsNilForUnknownExtensions { + id<GPBExtensionRegistry> globalRegistry = [GPBExtensionRegistry globalExtensionRegistry]; + // Known class, unknown field number. + XCTAssertNil([globalRegistry extensionForDescriptor:[TestAllExtensions descriptor] + fieldNumber:kUnusedExtensionFieldNumber]); + // A field number that has an entry, but for a different class. + XCTAssertNil([globalRegistry extensionForDescriptor:[TestAllTypes descriptor] + fieldNumber:kLookupExtensionFieldNumber]); +} + +- (void)testGlobalExtensionRegistryResolvesConcurrently { + // Unresolved, so the concurrent lookups below race to create the descriptor. + XCTAssertTrue(gConcurrentExtensionEntry.descriptor == nil); + + id<GPBExtensionRegistry> globalRegistry = [GPBExtensionRegistry globalExtensionRegistry]; + GPBDescriptor *extendedDescriptor = [TestAllExtensions descriptor]; + const size_t kIterationCount = 1000; + GPBExtensionDescriptor **concurrentResults = + calloc(kIterationCount, sizeof(GPBExtensionDescriptor *)); + dispatch_apply( + kIterationCount, dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^(size_t iteration) { + if (iteration % 2 == 0) { + concurrentResults[iteration] = + [globalRegistry extensionForDescriptor:extendedDescriptor + fieldNumber:kConcurrentExtensionFieldNumber]; + } else { + concurrentResults[iteration] = ResolveHandWrittenEntry(&gConcurrentExtensionEntry); + } + }); + + GPBExtensionDescriptor *resolved = gConcurrentExtensionEntry.descriptor; + XCTAssertNotNil(resolved); + for (size_t iteration = 0; iteration < kIterationCount; ++iteration) { + XCTAssertTrue(concurrentResults[iteration] == resolved); // ptr equality + } + free(concurrentResults); +} + +- (void)testGlobalExtensionRegistryParsesExtension { + XCTAssertTrue(gParseExtensionEntry.descriptor == nil); + + NSOutputStream *rawOutput = [NSOutputStream outputStreamToMemory]; + GPBCodedOutputStream *output = [GPBCodedOutputStream streamWithOutputStream:rawOutput]; + [output writeInt32:kParseExtensionFieldNumber value:7]; + [output flush]; + NSData *wireData = [rawOutput propertyForKey:NSStreamDataWrittenToMemoryStreamKey]; + XCTAssertNotNil(wireData); + + NSError *error = nil; + TestAllExtensions *withoutRegistry = [TestAllExtensions parseFromData:wireData error:&error]; + XCTAssertNotNil(withoutRegistry); + XCTAssertNil(error); + XCTAssertEqual(withoutRegistry.extensionsCurrentlySet.count, (NSUInteger)0); + // Parsing without a registry does not resolve the entry. + XCTAssertTrue(gParseExtensionEntry.descriptor == nil); + + TestAllExtensions *message = + [TestAllExtensions parseFromData:wireData + extensionRegistry:[GPBExtensionRegistry globalExtensionRegistry] + error:&error]; + XCTAssertNotNil(message); + XCTAssertNil(error); + GPBExtensionDescriptor *resolved = gParseExtensionEntry.descriptor; + XCTAssertNotNil(resolved); + NSArray *extensionsSet = message.extensionsCurrentlySet; + XCTAssertEqual(extensionsSet.count, (NSUInteger)1); + XCTAssertTrue(extensionsSet.firstObject == resolved); // ptr equality + XCTAssertTrue([message hasExtension:resolved]); + XCTAssertEqualObjects([message getExtension:resolved], @7); + XCTAssertEqualObjects(message.data, wireData); +} + +// Only runs when the test protos were generated with the +// `experimental_global_extension_registry` option in C function extension mode. +- (void)testGlobalExtensionRegistryRoundTripsGeneratedExtensions { + id<GPBExtensionRegistry> globalRegistry = [GPBExtensionRegistry globalExtensionRegistry]; + // optional_int32_extension = 1 in unittest.proto. + GPBExtensionDescriptor *optionalInt32Extension = + [globalRegistry extensionForDescriptor:[TestAllExtensions descriptor] fieldNumber:1]; + if (optionalInt32Extension == nil) { + XCTSkip(@"The test protos were generated without section entries."); + } +#if defined(GPB_UNITTEST_USE_C_FUNCTION_FOR_EXTENSIONS) + XCTAssertTrue(optionalInt32Extension == + Objc_Protobuf_Tests_extension_OptionalInt32Extension()); // ptr equality +#endif + + NSError *error = nil; + TestAllExtensions *allExtensions = [self allExtensionsSetRepeatedCount:kGPBDefaultRepeatCount]; + NSData *allExtensionsData = allExtensions.data; + XCTAssertNotNil(allExtensionsData); + TestAllExtensions *parsedAllExtensions = [TestAllExtensions parseFromData:allExtensionsData + extensionRegistry:globalRegistry + error:&error]; + XCTAssertNotNil(parsedAllExtensions); + XCTAssertNil(error); + [self assertAllExtensionsSet:parsedAllExtensions repeatedCount:kGPBDefaultRepeatCount]; + XCTAssertEqualObjects(parsedAllExtensions, allExtensions); + + TestPackedExtensions *packedExtensions = + [self packedExtensionsSetRepeatedCount:kGPBDefaultRepeatCount]; + NSData *packedExtensionsData = packedExtensions.data; + XCTAssertNotNil(packedExtensionsData); + TestPackedExtensions *parsedPackedExtensions = + [TestPackedExtensions parseFromData:packedExtensionsData + extensionRegistry:globalRegistry + error:&error]; + XCTAssertNotNil(parsedPackedExtensions); + XCTAssertNil(error); + [self assertPackedExtensionsSet:parsedPackedExtensions repeatedCount:kGPBDefaultRepeatCount]; + XCTAssertEqualObjects(parsedPackedExtensions, packedExtensions); +} + @end
diff --git a/objectivec/Tests/GPBGlobalExtensionRegistryProcessTest.m b/objectivec/Tests/GPBGlobalExtensionRegistryProcessTest.m new file mode 100644 index 0000000..01c5f0e --- /dev/null +++ b/objectivec/Tests/GPBGlobalExtensionRegistryProcessTest.m
@@ -0,0 +1,100 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google Inc. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +// The only test in its test targets (macos_ and ios_global_extension_registry_ +// process_test), so it runs in a fresh process in which nothing has used +// +[GPBExtensionRegistry globalExtensionRegistry] yet. That makes it possible +// to check, deterministically, that the global registry reads no Mach-O +// section before its first lookup, and to load a dynamic library after that +// first lookup. + +#import <XCTest/XCTest.h> +#import <dlfcn.h> + +#import "GPBDescriptor_PackagePrivate.h" +#import "GPBExtensionRegistry.h" +#import "GPBUtilities_PackagePrivate.h" +#import "objectivec/Tests/Unittest.pbobjc.h" + +// Enum constants, so they can be used in the static initializer. +enum { + // The extension in libGPBTestDynamicExtensionImage.dylib + // (GPBTestDynamicExtensionImage.m). + kDynamicImageExtensionFieldNumber = 30101, +}; + +typedef GPBExtensionDescriptor *(*GPBTestExtensionFunction)(void); +typedef GPBExtensionSectionEntry *(*GPBTestExtensionEntryFunction)(void); + +@interface GPBGlobalExtensionRegistryProcessTest : XCTestCase +@end + +@implementation GPBGlobalExtensionRegistryProcessTest + +// One test method, because the steps depend on the process wide state left by +// the previous ones. +- (void)testGlobalExtensionRegistryLifecycle { + GPBDescriptor *extendedDescriptor = [TestAllExtensions descriptor]; + + // 1. Getting the global registry reads no section. + XCTAssertFalse(GPBGlobalExtensionRegistryHasStartedIndexing()); + id<GPBExtensionRegistry> globalRegistry = [GPBExtensionRegistry globalExtensionRegistry]; + XCTAssertFalse(GPBGlobalExtensionRegistryHasStartedIndexing()); + + // 2. The first lookup on the global registry indexes the loaded images. The + // dynamic image is not loaded yet. + XCTAssertNil([globalRegistry extensionForDescriptor:extendedDescriptor + fieldNumber:kDynamicImageExtensionFieldNumber]); + XCTAssertTrue(GPBGlobalExtensionRegistryHasStartedIndexing()); + + // 3. Load the dynamic image. It resolves the protobuf runtime and + // TestAllExtensions from this test bundle through the flat namespace, so + // first make the (already loaded) test bundle's symbols globally visible. + NSBundle *testBundle = [NSBundle bundleForClass:[self class]]; + NSString *testBundleExecutablePath = testBundle.executablePath; + XCTAssertNotNil(testBundleExecutablePath); + void *testBundleHandle = dlopen(testBundleExecutablePath.fileSystemRepresentation, + RTLD_NOW | RTLD_NOLOAD | RTLD_GLOBAL); + XCTAssertTrue(testBundleHandle != NULL, @"%s", dlerror()); + NSString *imagePath = [testBundle pathForResource:@"libGPBTestDynamicExtensionImage" + ofType:@"dylib"]; + XCTAssertNotNil(imagePath); + if (imagePath == nil) { + return; + } + // Never closed: images with Objective-C references are not unloaded. + void *imageHandle = dlopen(imagePath.fileSystemRepresentation, RTLD_NOW | RTLD_LOCAL); + XCTAssertTrue(imageHandle != NULL, @"%s", dlerror()); + if (imageHandle == NULL) { + return; + } + GPBTestExtensionEntryFunction entryFunction = (GPBTestExtensionEntryFunction)dlsym( + imageHandle, "GPBTestDynamicExtensionImageExtensionEntry"); + GPBTestExtensionFunction extensionFunction = + (GPBTestExtensionFunction)dlsym(imageHandle, "GPBTestDynamicExtensionImageExtension"); + XCTAssertTrue(entryFunction != NULL); + XCTAssertTrue(extensionFunction != NULL); + if (entryFunction == NULL || extensionFunction == NULL) { + return; + } + GPBExtensionSectionEntry *entry = entryFunction(); + // Loading (and indexing) the image created no descriptor. + XCTAssertTrue(entry->descriptor == nil); + + // 4. The global registry now finds the image's extension: the instance its + // accessor returns. + GPBExtensionDescriptor *fromGlobalRegistry = + [globalRegistry extensionForDescriptor:extendedDescriptor + fieldNumber:kDynamicImageExtensionFieldNumber]; + XCTAssertNotNil(fromGlobalRegistry); + XCTAssertTrue(entry->descriptor == fromGlobalRegistry); // ptr equality + XCTAssertTrue(extensionFunction() == fromGlobalRegistry); // ptr equality + XCTAssertTrue(fromGlobalRegistry.containingMessageClass == [TestAllExtensions class]); + XCTAssertEqual(fromGlobalRegistry.fieldNumber, (uint32_t)kDynamicImageExtensionFieldNumber); +} + +@end
diff --git a/objectivec/Tests/GPBTestDynamicExtensionImage.m b/objectivec/Tests/GPBTestDynamicExtensionImage.m new file mode 100644 index 0000000..3617073 --- /dev/null +++ b/objectivec/Tests/GPBTestDynamicExtensionImage.m
@@ -0,0 +1,65 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google Inc. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +// Source of a dynamic library that GPBGlobalExtensionRegistryProcessTest.m +// loads with dlopen() to check that +[GPBExtensionRegistry +// globalExtensionRegistry] finds extensions from images loaded after its first +// lookup. +// +// The library is NOT linked against the protobuf runtime or the test protos +// (that would duplicate their classes). The runtime functions and the +// TestAllExtensions class are resolved from the test bundle at load time +// (`-undefined dynamic_lookup`). + +#import "GPBBootstrap.h" +#import "GPBDescriptor_PackagePrivate.h" +#import "GPBUtilities_PackagePrivate.h" + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdollar-in-identifier-extension" + +GPBObjCClassDeclaration(TestAllExtensions); + +// Must match kDynamicImageExtensionFieldNumber in +// GPBGlobalExtensionRegistryProcessTest.m. +static GPBExtensionDescription gDynamicImageExtensionDescription = { + .defaultValue.valueInt32 = 0, + .singletonName = "GPBTestDynamicExtensionImage_extension", + .extendedClass.clazz = GPBObjCClass(TestAllExtensions), + .messageOrGroupClass.clazz = Nil, + .enumDescriptorFunc = NULL, + .fieldNumber = 30101, + .dataType = GPBDataTypeInt32, + .options = GPBExtensionNone, +}; + +// The same form the generator emits with the +// `experimental_global_extension_registry` option. +GPB_EXTENSION_REGISTRY_SECTION_ATTR static GPBExtensionSectionEntry gDynamicImageExtensionEntry = { + .description = &gDynamicImageExtensionDescription, + .descriptor = nil, +}; + +#pragma clang diagnostic pop + +// Exported for GPBGlobalExtensionRegistryProcessTest.m, which looks them up +// with dlsym(). +__attribute__((visibility("default"))) GPBExtensionDescriptor * +GPBTestDynamicExtensionImageExtension(void); +__attribute__((visibility("default"))) GPBExtensionSectionEntry * +GPBTestDynamicExtensionImageExtensionEntry(void); + +// The extension's accessor, like a generated extension function. +GPBExtensionDescriptor *GPBTestDynamicExtensionImageExtension(void) { + return GPBResolveExtensionSectionEntry(&gDynamicImageExtensionEntry, + &GOOGLE_PROTOBUF_OBJC_EXPECTED_GENCODE_VERSION_40311); +} + +// The section entry, so the test can check whether its descriptor was created. +GPBExtensionSectionEntry *GPBTestDynamicExtensionImageExtensionEntry(void) { + return &gDynamicImageExtensionEntry; +}
diff --git a/src/google/protobuf/compiler/objectivec/extension.cc b/src/google/protobuf/compiler/objectivec/extension.cc index b1b7149..c531dfe 100644 --- a/src/google/protobuf/compiler/objectivec/extension.cc +++ b/src/google/protobuf/compiler/objectivec/extension.cc
@@ -165,6 +165,31 @@ absl::StrCat("GPBDataType", GetCapitalizedType(descriptor_))}, {"method_name", method_name_}, {"function_name", function_name_}, + {"create_and_return_descriptor", + [&] { + if (generation_options_.experimental_global_extension_registry) { + // See GPBDescriptor_PackagePrivate.h for details on section-based + // extension registration and GPBResolveExtensionSectionEntry(). + printer->Emit(R"objc( + GPB_EXTENSION_REGISTRY_SECTION_ATTR static GPBExtensionSectionEntry entry = { + .description = &description, + .descriptor = nil, + }; + return GPBResolveExtensionSectionEntry(&entry, &$google_protobuf_runtime_support$); + )objc"); + } else { + printer->Emit(R"objc( + static GPBExtensionDescriptor *extension; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + extension = + [[GPBExtensionDescriptor alloc] initWithExtensionDescription:&description + runtimeSupport:&$google_protobuf_runtime_support$]; + }); + return extension; + )objc"); + } + }}, {"number", descriptor_->number()}, {"options", BuildFlagsString(FLAGTYPE_EXTENSION, options)}, {"root_or_message_class_name", root_or_message_class_name_}, @@ -183,14 +208,7 @@ .dataType = $extension_type$, .options = $options$, }; - static GPBExtensionDescriptor *extension; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - extension = - [[GPBExtensionDescriptor alloc] initWithExtensionDescription:&description - runtimeSupport:&$google_protobuf_runtime_support$]; - }); - return extension; + $create_and_return_descriptor$; } )objc"); }
diff --git a/src/google/protobuf/compiler/objectivec/file.cc b/src/google/protobuf/compiler/objectivec/file.cc index 2a2176f..12f9b66 100644 --- a/src/google/protobuf/compiler/objectivec/file.cc +++ b/src/google/protobuf/compiler/objectivec/file.cc
@@ -409,17 +409,33 @@ } void FileGenerator::GenerateSource(io::Printer* p) const { - std::vector<const FileDescriptor*> deps_with_extensions = - common_state_->CollectMinimalFileDepsContainingExtensions(file_); + GenerateSourceImpl(p, /* include_root_and_extensions = */ true); +} + +void FileGenerator::GenerateSourceForMessagesAndEnums(io::Printer* p) const { + ABSL_CHECK(!is_bundled_proto_) + << "Bundled protos aren't expected to use multi source generation."; + GenerateSourceImpl(p, /* include_root_and_extensions = */ false); +} + +void FileGenerator::GenerateSourceImpl(io::Printer* p, + bool include_root_and_extensions) const { + std::vector<const FileDescriptor*> deps_with_extensions; GeneratedFileOptions file_options; - file_options.forced_files_to_import = deps_with_extensions; + if (include_root_and_extensions) { + deps_with_extensions = + common_state_->CollectMinimalFileDepsContainingExtensions(file_); + file_options.forced_files_to_import = deps_with_extensions; + } absl::btree_set<std::string> fwd_decls; for (const auto& generator : message_generators_) { generator->DetermineObjectiveCClassDefinitions(&fwd_decls); } - for (const auto& generator : extension_generators_) { - generator->DetermineObjectiveCClassDefinitions(&fwd_decls); + if (include_root_and_extensions) { + for (const auto& generator : extension_generators_) { + generator->DetermineObjectiveCClassDefinitions(&fwd_decls); + } } // The generated code for oneof's uses direct ivar access, suppress the @@ -443,7 +459,9 @@ GenerateFile(p, GeneratedFileType::kSource, file_options, [&] { EmitSourceFwdDecls(fwd_decls, p); - EmitRootImplementation(p, deps_with_extensions); + if (include_root_and_extensions) { + EmitRootImplementation(p, deps_with_extensions); + } EmitFileDescription(p); if (is_bundled_proto_ && HasWKTWithObjCCategory(file_)) { @@ -462,22 +480,17 @@ void FileGenerator::GenerateGlobalSource(io::Printer* p) const { ABSL_CHECK(!is_bundled_proto_) << "Bundled protos aren't expected to use multi source generation."; + ABSL_CHECK(generation_options_.extension_generation_mode == + ExtensionGenerationMode::kCFunction); std::vector<const FileDescriptor*> deps_with_extensions = common_state_->CollectMinimalFileDepsContainingExtensions(file_); GeneratedFileOptions file_options; file_options.forced_files_to_import = deps_with_extensions; - absl::btree_set<std::string> fwd_decls; - for (const auto& generator : extension_generators_) { - generator->DetermineObjectiveCClassDefinitions(&fwd_decls); - } - - if (!fwd_decls.empty()) { - file_options.ignored_warnings.push_back("dollar-in-identifier-extension"); - } - + // Each C-function extension is emitted in its own `.pbobjc.m` shard via + // `GenerateSourceForExtension()`, so `0.pbobjc.m` contains only the per-file + // `<File>_Registry()` function and needs no class forward declarations. GenerateFile(p, GeneratedFileType::kSource, file_options, [&] { - EmitSourceFwdDecls(fwd_decls, p); EmitRootImplementation(p, deps_with_extensions); }); } @@ -519,6 +532,34 @@ }); } +// Generates a dedicated `.pbobjc.m` source shard for the `idx`-th C-function +// extension in `extension_generators_` (covering both file-scoped and +// message-scoped extensions). +// +// Isolating each C-function extension in its own translation unit ensures that +// referencing a message class in the same `.proto` file (or referencing one +// extension in the file) never forces the static linker to pull in sibling or +// message-scoped extensions from the static archive. +void FileGenerator::GenerateSourceForExtension(size_t idx, + io::Printer* p) const { + ABSL_CHECK(!is_bundled_proto_) + << "Bundled protos aren't expected to use multi source generation."; + const auto& generator = extension_generators_[idx]; + + absl::btree_set<std::string> fwd_decls; + generator->DetermineObjectiveCClassDefinitions(&fwd_decls); + + GeneratedFileOptions file_options; + if (!fwd_decls.empty()) { + file_options.ignored_warnings.push_back("dollar-in-identifier-extension"); + } + + GenerateFile(p, GeneratedFileType::kSource, file_options, [&] { + EmitSourceFwdDecls(fwd_decls, p); + generator->GenerateDescriptorFunction(p); + }); +} + void FileGenerator::GenerateFile(io::Printer* p, GeneratedFileType file_type, const GeneratedFileOptions& file_options, std::function<void()> body) const { @@ -900,8 +941,17 @@ { "extension_descriptor_functions", [&] { - for (const auto& generator : extension_generators_) { - generator->GenerateDescriptorFunction(p); + // In single-source mode, emit all C-function extension descriptor + // definitions directly in the single `.pbobjc.m` file. In + // multi-source mode, each extension descriptor function is emitted + // in its own dedicated `.pbobjc.m` shard via + // `GenerateSourceForExtension()`, leaving only `<File>_Registry()` + // in `0.pbobjc.m` so `0.pbobjc.o` can be dead-stripped when + // `<File>_Registry()` is unreferenced. + if (!generation_options_.IsMultiSourceGeneration()) { + for (const auto& generator : extension_generators_) { + generator->GenerateDescriptorFunction(p); + } } }, }},
diff --git a/src/google/protobuf/compiler/objectivec/file.h b/src/google/protobuf/compiler/objectivec/file.h index 495aa63..b67208d 100644 --- a/src/google/protobuf/compiler/objectivec/file.h +++ b/src/google/protobuf/compiler/objectivec/file.h
@@ -69,10 +69,22 @@ size_t NumEnums() const { return enum_generators_.size(); } size_t NumMessages() const { return message_generators_.size(); } + // Returns the total number of extensions (both file-scoped and + // message-scoped) defined in this `.proto` file. + size_t NumExtensions() const { return extension_generators_.size(); } + // Generates the file's root class and/or extension registry function. void GenerateGlobalSource(io::Printer* p) const; + // Generates the source for the `idx`-th message in the file. void GenerateSourceForMessage(size_t idx, io::Printer* p) const; + // Generates the source for all enums in the file. void GenerateSourceForEnums(io::Printer* p) const; + // Generates the source for all the messages and enums of the file (without + // the root class, extension registry or extension descriptor functions) for + // the "extensions_only" multi-source generation mode. + void GenerateSourceForMessagesAndEnums(io::Printer* p) const; + // Generates the source for the `idx`-th C-function extension in the file. + void GenerateSourceForExtension(size_t idx, io::Printer* p) const; private: enum class GeneratedFileType : int { kHeader, kSource }; @@ -82,6 +94,13 @@ std::vector<std::string> extra_system_headers; }; + // Shared implementation of GenerateSource() and + // GenerateSourceForMessagesAndEnums(). `include_root_and_extensions` + // controls if the root class/extension registry and the extension descriptor + // functions are included. + void GenerateSourceImpl(io::Printer* p, + bool include_root_and_extensions) const; + void GenerateFile(io::Printer* p, GeneratedFileType file_type, const GeneratedFileOptions& file_options, std::function<void()> body) const;
diff --git a/src/google/protobuf/compiler/objectivec/generator.cc b/src/google/protobuf/compiler/objectivec/generator.cc index fdc5dd1..91c43bf 100644 --- a/src/google/protobuf/compiler/objectivec/generator.cc +++ b/src/google/protobuf/compiler/objectivec/generator.cc
@@ -279,17 +279,28 @@ // Enables a mode where the generated source is split across multiple .m // files; this is to explore impacts on code size when not // compiling/linking with `-ObjC` as then only linker visible needs should - // be pulled into the builds. The available options are: - // "all": Each ObjC class (messages and roots) generates to a unique .m - // file. + // be pulled into the builds. Multi-source generation requires + // `extension_generation_mode=c_function`. The available options are: + // "all": Each ObjC message class, the file's extension registry + // function, and each extension descriptor function generates to a + // unique .m file. + // "extensions_only": The messages and enums of a file generate to one + // .m file (the same code as without multi source generation). The + // file's extension registry function and each extension descriptor + // function generate to their own .m files. This keeps the classes of + // a file together, so not linking with `-ObjC` (or `alwayslink`) only + // changes which extensions get linked. // Without this option, each .proto file generates a single .m file. if (options[i].second == "all") { generation_options.experimental_multi_source_generation = MultiSourceGenerationMode::kAll; + } else if (options[i].second == "extensions_only") { + generation_options.experimental_multi_source_generation = + MultiSourceGenerationMode::kExtensionsOnly; } else { *error = absl::StrCat( "error: Unknown value for experimental_multi_source_generation. " - "Expected 'all'. Got: '", + "Expected one of 'all' or 'extensions_only'. Got: '", options[i].second, "'"); return false; } @@ -303,6 +314,25 @@ options[i].second); return false; } + } else if (options[i].first == "experimental_global_extension_registry") { + // This is an experimental option, and could be removed or change at any + // time; it is not documented in the README.md for that reason. + // + // When enabled, each C function extension records its description in the + // extension registration Mach-O section (see + // GPBDescriptor_PackagePrivate.h), so the extension can be found by + // +[GPBExtensionRegistry globalExtensionRegistry] if it is linked into + // the process. Requires `extension_generation_mode=c_function`. Default + // is "no", which generates the same code as before this option existed. + if (!StringToBool( + options[i].second, + &generation_options.experimental_global_extension_registry)) { + *error = absl::StrCat( + "error: Unknown value for " + "experimental_global_extension_registry: ", + options[i].second); + return false; + } } else if (options[i].first == "annotation_pragma_name") { generation_options.annotation_pragma_name = options[i].second; } else if (options[i].first == "annotation_guard_name") { @@ -344,16 +374,32 @@ } } - // Multi source generation forces: + // Multi source generation requires C function extensions and forces: // - off the use of fwd decls in favor of imports // - on the minimal imports support if (generation_options.IsMultiSourceGeneration()) { + if (generation_options.extension_generation_mode != + ExtensionGenerationMode::kCFunction) { + *error = + "error: experimental_multi_source_generation requires " + "extension_generation_mode=c_function."; + return false; + } generation_options.headers_use_forward_declarations = false; generation_options.generate_minimal_imports = true; } if (generation_options.experimental_strip_nonfunctional_codegen) { generation_options.generate_minimal_imports = true; } + if (generation_options.experimental_global_extension_registry) { + if (generation_options.extension_generation_mode != + ExtensionGenerationMode::kCFunction) { + *error = + "error: experimental_global_extension_registry requires " + "extension_generation_mode=c_function."; + return false; + } + } // ----------------------------------------------------------------- @@ -484,62 +530,112 @@ // Generate m file(s). if (!headers_only && skip_impls.count(file->name()) == 0) { - if (generation_options.IsMultiSourceGeneration()) { - int file_number = 0; - - // Generate the Root and FileDescriptor (if needed). - { - std::unique_ptr<io::ZeroCopyOutputStream> output( - context->Open(NumberedObjCMFileName(filepath, file_number++))); + int file_number = 0; + switch (generation_options.experimental_multi_source_generation) { + case MultiSourceGenerationMode::kNone: { + // Normal single-file generation: everything goes into one .pbobjc.m. + auto output = absl::WrapUnique( + context->Open(absl::StrCat(filepath, ".pbobjc.m"))); io::Printer printer(output.get()); - file_generator.GenerateGlobalSource(&printer); + file_generator.GenerateSource(&printer); if (printer.failed()) { *error = absl::StrCat( "error: internal error generating an implementation:", file->name()); return false; } + break; } - // Enums only generate C functions, so they can all go in one file as - // dead stripping anything not used. - if (file_generator.NumEnums() > 0) { - std::unique_ptr<io::ZeroCopyOutputStream> output( - context->Open(NumberedObjCMFileName(filepath, file_number++))); - io::Printer printer(output.get()); - file_generator.GenerateSourceForEnums(&printer); - if (printer.failed()) { - *error = absl::StrCat( - "error: internal error generating an enum implementation(s):", - file->name()); - return false; + case MultiSourceGenerationMode::kExtensionsOnly: + case MultiSourceGenerationMode::kAll: { + // Generate the file's extension registry function in its own file so + // that referencing a message or enum of the file does not also pull + // in the registry (and through it every extension of the file and of + // its imports). + { + std::unique_ptr<io::ZeroCopyOutputStream> output( + context->Open(NumberedObjCMFileName(filepath, file_number++))); + io::Printer printer(output.get()); + file_generator.GenerateGlobalSource(&printer); + if (printer.failed()) { + *error = absl::StrCat( + "error: internal error generating an implementation:", + file->name()); + return false; + } } - } - for (size_t i = 0; i < file_generator.NumMessages(); ++i) { - std::unique_ptr<io::ZeroCopyOutputStream> output( - context->Open(NumberedObjCMFileName(filepath, file_number++))); - io::Printer printer(output.get()); - file_generator.GenerateSourceForMessage(i, &printer); - if (printer.failed()) { - *error = absl::StrCat( - "error: internal error generating an message implementation:", - file->name(), "::", i); - return false; + if (generation_options.experimental_multi_source_generation == + MultiSourceGenerationMode::kExtensionsOnly) { + // The messages and enums (and the FileDescriptor) stay together in + // one file, the same as without multi source generation. + if (file_generator.NumEnums() > 0 || + file_generator.NumMessages() > 0) { + std::unique_ptr<io::ZeroCopyOutputStream> output(context->Open( + NumberedObjCMFileName(filepath, file_number++))); + io::Printer printer(output.get()); + file_generator.GenerateSourceForMessagesAndEnums(&printer); + if (printer.failed()) { + *error = absl::StrCat( + "error: internal error generating a message/enum " + "implementation(s):", + file->name()); + return false; + } + } + } else { + // Enums only generate C functions, so they can all go in one file + // as dead stripping anything not used. + if (file_generator.NumEnums() > 0) { + std::unique_ptr<io::ZeroCopyOutputStream> output(context->Open( + NumberedObjCMFileName(filepath, file_number++))); + io::Printer printer(output.get()); + file_generator.GenerateSourceForEnums(&printer); + if (printer.failed()) { + *error = absl::StrCat( + "error: internal error generating an enum " + "implementation(s):", + file->name()); + return false; + } + } + + for (size_t i = 0; i < file_generator.NumMessages(); ++i) { + std::unique_ptr<io::ZeroCopyOutputStream> output(context->Open( + NumberedObjCMFileName(filepath, file_number++))); + io::Printer printer(output.get()); + file_generator.GenerateSourceForMessage(i, &printer); + if (printer.failed()) { + *error = absl::StrCat( + "error: internal error generating an message " + "implementation:", + file->name(), "::", i); + return false; + } + } + } // if (kExtensionsOnly) ... else (kAll) + + // Emit a separate numbered `.pbobjc.m` shard for each extension + // (both file-scoped and message-scoped) so each extension can be + // independently dead-stripped by the linker unless its C accessor + // function is explicitly referenced. + for (size_t i = 0; i < file_generator.NumExtensions(); ++i) { + std::unique_ptr<io::ZeroCopyOutputStream> output( + context->Open(NumberedObjCMFileName(filepath, file_number++))); + io::Printer printer(output.get()); + file_generator.GenerateSourceForExtension(i, &printer); + if (printer.failed()) { + *error = absl::StrCat( + "error: internal error generating an extension " + "implementation:", + file->name(), "::", i); + return false; + } } + break; } - } else { - auto output = absl::WrapUnique( - context->Open(absl::StrCat(filepath, ".pbobjc.m"))); - io::Printer printer(output.get()); - file_generator.GenerateSource(&printer); - if (printer.failed()) { - *error = absl::StrCat( - "error: internal error generating an implementation:", - file->name()); - return false; - } - } + } // switch (generation_options.experimental_multi_source_generation) } // if (!headers_only && skip_impls.count(file->name()) == 0) } // for(file : files)
diff --git a/src/google/protobuf/compiler/objectivec/options.h b/src/google/protobuf/compiler/objectivec/options.h index 57e4113..913cc0c 100644 --- a/src/google/protobuf/compiler/objectivec/options.h +++ b/src/google/protobuf/compiler/objectivec/options.h
@@ -41,6 +41,11 @@ // Each ObjC class (messages and roots) generates to a unique .m file. kAll, + + // Messages and enums are generated into one .m file (the same code as single + // source generation), and only the extension registry/root and the extension + // descriptor functions are split into their own .m files. + kExtensionsOnly, }; // Generation options, documented within generator.cc. @@ -58,6 +63,7 @@ MultiSourceGenerationMode experimental_multi_source_generation = MultiSourceGenerationMode::kNone; bool experimental_strip_nonfunctional_codegen = false; + bool experimental_global_extension_registry = false; // The name of the pragma that will be used to indicate the start of the // metadata annotations. Must be set (along with `annotation_guard_name`) for