Skip to content

Share multipart framing and headers with Kotlin Multiplatform - #58474

Open
kunal26das wants to merge 9 commits into
react:mainfrom
kunal26das:codex/kmp-multipart-stack
Open

Share multipart framing and headers with Kotlin Multiplatform#58474
kunal26das wants to merge 9 commits into
react:mainfrom
kunal26das:codex/kmp-multipart-stack

Conversation

@kunal26das

@kunal26das kunal26das commented Sep 10, 2026

Copy link
Copy Markdown

Summary

Native stacked review: Review this layer in the registered six-PR fork stack. This PR remains the upstream submission to react/react-native.

Share multipart framing and raw header splitting between Android's MultipartStreamReader and Apple's RCTMultipartStreamReader. This is the second use case in the Kotlin Multiplatform series: foundation #58472, then gradients #58456; explicit scroll offsets follow in #58475. The independent Android header-boundary defect is fixed separately in #58473; RNTester's source-build dependency fix is #58471.

Review this layer: multipart changes above the native bounds fix (20 paths, including tests and runtime-owner integration). These are dependent fork branches targeting upstream main, so GitHub's full PR diff includes the prerequisites until they land.

The shared implementation is 79 lines. Native code searches its existing Okio/NSData buffers and passes scalar positions to a common state machine, which decides delimiter overlap, preamble handling and completed parts. A second common function splits raw header fields. Body bytes do not cross the Kotlin/Objective-C boundary. Android retains case-insensitive header keys and its trimming rules; Apple retains its dictionary and whitespace behavior. Stream ownership, reads, progress timing, body delivery and callbacks stay native. There is no Compose dependency or JavaScript API change.

Runtime ownership

Multipart lives in React-Core, whereas gradients live in Fabric. For dynamic CocoaPods frameworks, this layer moves ownership of the single static Kotlin archive to React-Core and lets Fabric import that owner. This avoids a React-Core-to-Fabric dependency. React-Core uses -ObjC to retain shared Objective-C classes even when it does not call every algorithm itself. Static-library/framework applications continue to own the runtime once. Hosted-test ownership and its binary guard are established in the gradient prerequisite, including metadata-free sibling tests; this layer preserves that protection. Generated graph tests include consumers using React-Core without Fabric.

Android continues to embed the same shared JVM module in the ReactAndroid AAR. Apple remains an opt-in CocoaPods source-build experiment through RCT_USE_KMP=1. Catalyst keeps the native implementation; full React Native SwiftPM/prebuilt adoption remains guarded.

Feasibility and costs

This boundary amortizes interop over buffer reads and avoids transferring the payload into Kotlin. It is a stronger candidate than a utility requiring per-element Objective-C calls. C++ could also share the decisions and buffer scanning; it would require a JNI interface for this currently Kotlin Android consumer. Neither choice eliminates native I/O or the Catalyst fallback.

Three independent optimized Apple simulator parser runs, with input construction excluded and matched 2 MiB/20 MiB payloads, measured KMP/native median ratios of 1.016–1.033. The additional time was approximately 0.03–0.06 ms at 2 MiB and 0.35–0.60 ms at 20 MiB. Exact body and callback parity passed. These are parser measurements on an ARM64 simulator, not network throughput or physical-device results.

Fresh host-JVM measurements use the repository’s Okio 2.9.0 and Kotlin 2.2.0 standard library, with the standalone Kotlin 2.4.20 compiler retaining language/API level 2.2. Three fresh processes each used 30 warmups and 41 alternating native/KMP samples per payload. Per-process KMP/native median ratios ranged from 0.986–1.005; the median ratios across processes were 0.993 at 2 MiB and 0.999 at 20 MiB. There is no consistent timing advantage. Measured per-parse thread allocations were 11,344 → 10,072 bytes at 2 MiB and 11,360 → 10,088 bytes at 20 MiB; input construction is excluded. These are host-JVM measurements, not Android ART, network throughput or whole-application memory. The matching fixture also passes nine adapter tests for each implementation. Task-owned builds were stopped during measurement.

App startup, installed size and representative bundle-download memory still need measurement. The runtime overhead reported in the gradient PR remains relevant; this PR does not justify adoption on speed alone.

Changelog:

[GENERAL] [CHANGED] - Share multipart framing and raw header splitting on Android and opt-in KMP iOS source builds.

Test Plan

Runtime-ownership coverage includes nested and full-pod sibling XCTest bundles, optional host metadata, and standalone/non-KMP-host controls. The retained three-use-case application runs at 9cc8379 each passed 154 active tests under Address Sanitizer and launched successfully; fresh focused checks cover the updated host resolver.

At exact head d14ae50dc89f, small CocoaPods/Xcode projects pass five real XCTest cases in each static-library and static-framework mode on the ARM64 iOS simulator. They call gradient and multipart APIs, including host-created results. Metadata-present, metadata-free and SDK-dependent hosted bundles define zero RNS classes; standalone and non-KMP-host tests retain their own runtime. Both modes have zero failures/skips or duplicate-runtime warnings. Real Xcode settings and generated device/simulator link flags verify the SDK-dependent host selection; no device execution is claimed.

From packages/react-native/ReactShared with JDK 17 and Xcode 26.6/iOS 26.5:

./gradlew jvmTest iosSimulatorArm64Test --max-workers=2
python3 scripts/test-android-multipart.py --baseline-ref fc402a6b72034ec4e0edf2dfcc0f2f506904c4e3
./scripts/test-apple-multipart.sh
bundle exec ruby scripts/test-cocoapods-linking.rb
./scripts/test-apple-app.sh simulator
USE_FRAMEWORKS=static ./scripts/test-apple-app.sh simulator
USE_FRAMEWORKS=dynamic ./scripts/test-apple-app.sh simulator
./scripts/test-apple-app.sh device
./scripts/test-apple-app.sh catalyst
  • Ten new common multipart tests passed on JVM and ARM64 iOS simulator, alongside the 22 gradient tests.

  • Actual Android adapter: nine tests passed against both the shared implementation and fixed native baseline, including every read boundary, binary bytes, malformed/truncated input, header rules and callback traces. The updated fixture also passes with the repository dependency versions.

  • Actual Apple adapter: seven XCTest cases passed with each implementation, plus 232 exact callback/body parity cases. The Catalyst object compiles without shared-runtime references.

  • Fresh CocoaPods 1.14.2 and 1.15.2 graph runs at d14ae50dc89f each pass 112 configurations and 224 per-SDK archive-link assertions. They cover all gradient-layer host regressions plus a React-Core-only consumer: static, dynamic and mixed linkage, optional/stale metadata, custom/SDK-specific products, standalone tests, and non-KMP hosts with unrelated KMP dependencies. These are project-generation checks, not 112 application builds.

  • The assembled three-use-case stack passed 623 ReactAndroid unit tests across 94 suites and built the ARM64 RNTester Release APK with 16 KiB alignment.

  • Previously validated local Maven consumers and packed-npm source consumers passed Debug and R8 Release. Source consumption builds standard Hermes from source. Five AARs contain each of the 12 shared class entries exactly once, and bytecode checks confirm actual calls from all three use cases.

  • Both Release consumers passed real gradient and multipart assertions on ARM64 API 24, 26 and 37. A separate R8 fixture passed six explicit-offset assertions per API using the real vertical, horizontal and nested views, including retained-list updates. RNTester exercised ten gradient routes and three normal scroll routes on each API 26/37, plus two gradient routes on API 24. These are targeted assertions and UI smoke, not exhaustive gestures or pixel comparisons.

  • Retained static-library and static-framework RNTester runs at 9cc8379 each passed 154 tests, with 16 upstream skips and zero failures under Address Sanitizer, and launched. These repeat the same suite, not 308 distinct tests. Compiled adapters call all three shared APIs. The application owns one runtime; hosted integration tests import it, while standalone unit tests own their runtime separately.

  • Universal Catalyst source build and actual ARM64/Rosetta x86_64 launches passed representative linear/radial rendering checks. All six compiled adapter objects use native fallback, and the final binaries have no shared Kotlin runtime. This is Catalyst fallback coverage, not Intel iOS simulator execution.

  • Four independent Kotlin runtime/static-dynamic combinations passed. An empty Objective-C owner linked with -ObjC retains all shared APIs. A separate licensed XCFramework/SwiftPM distribution consumer builds for device/simulator/Catalyst and passes one ARM64 simulator test, reusing the matching Release framework archives. This does not enable complete React Native core SwiftPM/prebuilt adoption.

  • The final assembled package includes all 44 shared-module files and excludes shared generated build outputs and caches.

  • Retained unsigned iOS ARM64 Release source build at 9cc8379 passed. All three actual adapter objects call their shared APIs, and the app executable owns one Kotlin runtime. No physical-device execution is claimed.

  • The assembled stack's dynamic-framework RNTester run passed 154 tests, with 16 upstream skips and no failures, and launched successfully. Actual compiled gradient, multipart and scroll adapters reference the shared APIs. The app load graph has one runtime owner, React.framework; Fabric imports it. Byte-identical React.framework copies packaged with XCTest bundles are recorded separately.

Android app validation uses 586f67bafb46 plus the regression’s Okio annotation; its production algorithms, adapters and build inputs match the final stack. Retained Apple static-library, static-framework, unsigned device and Catalyst validation uses 9cc8379d0ad8. The current changes refine hosted-test resolution in CocoaPods and move its protection into the gradient prerequisite; they do not change the shared algorithms or adapters. Dynamic execution is retained from 586f67bafb46: the dynamic helper path returns before the changed host logic; the dynamic runtime owner is unchanged, and its preserved binaries passed the ownership guard. It is not a new final-commit dynamic build or run. Common/framework validation at 265de92 and separate distribution/runtime validation at 772c54b have unchanged production inputs. These application checks cover all three use cases together.

Intel simulator execution is deferred; no physical devices were tested. Full React Native core SwiftPM/prebuilt adoption remains outside this experiment. Hosted workflows still require upstream approval where GitHub gates fork runs.

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Sep 10, 2026
@facebook-github-tools facebook-github-tools Bot added the Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team. label Sep 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant