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, &sectionSize);
+  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