Skip to content

[SwiftExtract] Preserve source file path for deferred (out-of-order) extensions - #924

Merged
ktoso merged 3 commits into
swiftlang:mainfrom
caggiulio:bugfix/extension-files
Sep 25, 2026
Merged

ktoso merged 3 commits into
swiftlang:mainfrom
caggiulio:bugfix/extension-files

Conversation

@caggiulio

@caggiulio caggiulio commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Problem

jextract's symbol table builder processes source files in a single pass, in
whatever order FileManager enumerates the input directory. If an
extension Outer.Inner { ... } is visited before the file that declares
Outer (or Inner), its extended type can't be resolved yet, so it's queued
in unresolvedExtensions and retried in finalize().

When finalize() resolves a deferred extension, it currently hardcodes the
source file path as "FIXME_MISSING_FILEPATH.swift"
(SwiftParsedModuleSymbolTableBuilder.swift, with a // FIXME comment
already marking this).

Because the JNI thunk printer groups generated thunks by source file
(writeSwiftThunkSources), everything with this fake path lands in a single
FIXME_MISSING_FILEPATH+SwiftJava.swift. The JExtractSwiftPlugin only
declares the real per-source-file +SwiftJava.swift outputs to SwiftPM, so
this file is never compiled. The generated Java bindings are correct (the
Java class exists with all its methods, including native declarations), but
the corresponding Swift/JNI implementations are silently missing from the
built library — this fails at runtime with UnsatisfiedLinkError /
"No implementation found for ..." instead of at build time.

This is order-dependent, so it's easy to hit by accident: on a case-insensitive
APFS volume, FileManager's directory enumeration order is filename-hash
based, not alphabetical, so whether an extension's file is read before or
after its extended type's declaring file depends on unrelated filenames
elsewhere in the module. For example, extension Outer.Inner { struct X {} }
in Feature.swift and enum Outer { enum Inner {} } in Core.swift work
fine as long as Core.swift happens to be enumerated first — flip that
order (as can happen simply by adding or renaming unrelated files in the
same directory) and X's JNI thunks silently disappear from the build.

Fix

  1. SwiftParsedModuleSymbolTableBuilder.unresolvedExtensions now stores the
    sourceFilePath alongside each deferred ExtensionDeclSyntax, and
    finalize() passes that real path through to tryHandle(extension:sourceFilePath:)
    instead of the placeholder string. Deferred extensions are now attributed
    to their actual source file like any other declaration, so their JNI thunks
    land in the correct, plugin-declared output file and actually get compiled.

  2. collectAllFiles now sorts the files gathered from each input path before
    returning them. Enumeration order was previously whatever the filesystem
    handed back (non-deterministic across machines/filesystems for the reasons
    above); sorting makes extraction output reproducible. This alone doesn't
    fix the bug (many still-valid orderings would still defer extensions
    declared in files before their type's declaring file), but it removes
    filesystem/OS as a source of flakiness on top of fix (1).

Testing

Added deferredExtensionKeepsSourceFilePath to
Tests/SwiftExtractTests/SwiftSymbolTableTests.swift: it feeds the symbol
table an extension file before the file declaring the extended type (mirroring
the real bug), then asserts the nested type's sourceFilePath matches the
extension's actual file rather than the fixed fallback.

Ran the full SwiftExtractTests suite (swift test --filter SwiftExtractTests):
91 tests, 11 suites, all passing.

Verified end-to-end against a real multi-target Swift package with a
JExtractSwiftPlugin-driven Android build: before the fix, HomeFeed's
JNI thunks (and everything else in the affected files) were generated into
FIXME_MISSING_FILEPATH+SwiftJava.swift and silently dropped from the build;
after the fix they're written to their real per-file output and the resulting
.so exports the expected JNI symbols.

@caggiulio
caggiulio requested a review from ktoso as a code owner September 24, 2026 18:47
Comment thread Sources/SwiftExtract/SwiftTypes/SwiftParsedModuleSymbolTableBuilder.swift Outdated
@ktoso

ktoso commented Sep 24, 2026

Copy link
Copy Markdown
Collaborator

Thanks for the contribution, looks fine I think.

One thing I'd like to request for the future though is to cut down on the amount of wording in a PR description, it's a bit of a wall of text -- it is much easier to review if you summarize the problem using your own words in a few minimal sentences.

@caggiulio
caggiulio force-pushed the bugfix/extension-files branch from 0d2c6b6 to b0bb56c Compare September 25, 2026 07:22
@caggiulio

Copy link
Copy Markdown
Contributor Author

Thanks for the contribution, looks fine I think.

One thing I'd like to request for the future though is to cut down on the amount of wording in a PR description, it's a bit of a wall of text -- it is much easier to review if you summarize the problem using your own words in a few minimal sentences.

Sorry for the wall of text! It’s a habit of mine to write detailed PR descriptions. In my projects, I tend to include a lot of information to build solid documentation and provide context for future reference.

I’ll keep it more concise in the future. Thanks for the tip!

@caggiulio
caggiulio requested a review from ktoso September 25, 2026 08:33
@ktoso
ktoso merged commit 60b0d18 into swiftlang:main Sep 25, 2026
41 of 42 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants