Skip to content

fix(widget): emit shared SwiftUI FFI helpers once per bundle (#5069)#5119

Merged
proggeramlug merged 3 commits into
mainfrom
claude/issue-5069-v6k5t6
Jun 14, 2026
Merged

fix(widget): emit shared SwiftUI FFI helpers once per bundle (#5069)#5119
proggeramlug merged 3 commits into
mainfrom
claude/issue-5069-v6k5t6

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #5069. A perry/widget bundle with ≥2 Widget({...}) declarations failed to compile under swiftc *.swift.

The shared perry-runtime FFI helpers (perry_runtime_widget_init, the two perry_nanbox_string overloads, perry_get_string_ptr/perry_get_string) plus the @_cdecl("perry_widget_shared_storage_get") bridge were emitted once per widget, into each {Name}Glue.swift. Since a widget bundle compiles as a single Swift module, neither per-file visibility worked:

The @_cdecl storage bridge exports a fixed C symbol, so ≥2 widgets sharing an app group would also have duplicated it at link time.

Fix

  • perry-codegen-swiftui/src/emit.rs — split emit_glue into emit_shared_runtime(app_group) (the FFI block + @_cdecl bridge, now internal) and a slimmed emit_glue that keeps only the widget-unique @_silgen_name provider import.
  • perry-codegen-swiftui/src/lib.rscompile_widget emits per-widget glue only when provider_func_name is set; exposes emit_shared_runtime.
  • perry/src/commands/compile/targets.rs — both compile_for_ios_widget and compile_for_watchos_widget write PerryWidgetRuntime.swift once after the per-widget loop, gated on any widget using a native provider or shared storage, with the bundle-wide app group resolved from the first widget that configures one.

Tests

Added 3 unit tests covering the shared-runtime split (internal visibility, app-group storage bridge, per-widget glue carrying only the provider extern). Full crate suite (18 tests) passes; cargo build --release -p perry is clean.

Note

The downstream .appex link step from the issue (pulling in libperry_runtime.a / the TS provider objects) needs a macOS/Xcode toolchain to verify. This change unblocks the swiftc compile so that step is now reachable to confirm.

https://claude.ai/code/session_01JJfeShA3WCh9HMiEBmqq39


Generated by Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Added once-per-bundle generation of PerryWidgetRuntime.swift during widget compilation, including shared runtime helpers and an app-group-based shared storage bridge when needed.
  • Refactor

    • Per-widget glue is emitted only for widgets using a native provider.
    • Shared runtime/store bridge emission happens exactly once per widget bundle; conflicting app-group values in the same bundle now produce a warning and the first app group is used.
  • Tests

    • Updated unit tests to validate the conditional shared-runtime output and ensure it’s omitted from per-widget output.

@coderabbitai

coderabbitai Bot commented Jun 14, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e0649588-0d99-46ab-94f4-dd6c10652dae

📥 Commits

Reviewing files that changed from the base of the PR and between 431f60b and 6ee593e.

📒 Files selected for processing (1)
  • crates/perry/src/commands/compile/targets.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/perry/src/commands/compile/targets.rs

📝 Walkthrough

Walkthrough

Introduces emit_shared_runtime(app_group: Option<&str>) to emit bundle-scoped Swift FFI helpers and the optional @_cdecl shared-storage bridge into a single PerryWidgetRuntime.swift. Removes these from per-widget emit_glue, which now emits only the provider @_silgen_name extern. The iOS and watchOS widget compile drivers conditionally write this file once per bundle.

Changes

Shared Swift FFI runtime extraction

Layer / File(s) Summary
emit_shared_runtime + emit_glue refactor + tests
crates/perry-codegen-swiftui/src/emit.rs
Adds emit_shared_runtime(app_group) emitting bundle-scoped Swift FFI imports, runtime wrappers, and (conditionally) the @_cdecl("perry_widget_shared_storage_get") bridge backed by UserDefaults(suiteName:). Refactors emit_glue to emit only the per-widget @_silgen_name provider extern. Adds tests covering non-private helpers in shared runtime, conditional bridge emission, and absence of shared helpers from per-widget glue.
Public API + glue condition narrowing
crates/perry-codegen-swiftui/src/lib.rs
Exposes pub fn emit_shared_runtime(app_group: Option<&str>) -> String with documentation that it must be called exactly once per bundle. Narrows per-widget glue emission to depend only on provider_func_name, removing the prior app_group dependency.
Bundle-level PerryWidgetRuntime.swift emission
crates/perry/src/commands/compile/targets.rs
Introduces write_shared_widget_runtime helper and integrates it into both compile_for_ios_widget and compile_for_watchos_widget, conditionally calling emit_shared_runtime once when any widget declares a provider function or app group, writing PerryWidgetRuntime.swift to the output directory, and appending it to Swift compilation inputs. Warns via eprintln! if multiple widgets declare differing app groups.

Sequence Diagram(s)

sequenceDiagram
    participant Driver as compile_for_ios/watchos_widget
    participant Codegen as perry_codegen_swiftui
    participant FS as Output Directory

    loop for each widget
        Driver->>Codegen: compile_widget(widget)
        Codegen-->>FS: {Name}.swift + {Name}Glue.swift
    end

    Driver->>Driver: scan all widgets for provider_func_name or app_group
    alt any widget has provider/app_group
        Driver->>Codegen: emit_shared_runtime(first_app_group)
        Codegen-->>Driver: PerryWidgetRuntime.swift source
        Driver->>FS: write PerryWidgetRuntime.swift
        Driver->>Driver: append to Swift compilation inputs
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐇 Hop hop, the glue was spreading wide,
Private helpers hiding deep inside,
One swift file now holds the runtime tight,
Emitted once per bundle — finally right!
Duplicate symbols scatter and flee,
The widget extension compiles with glee! 🎉

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix(widget): emit shared SwiftUI FFI helpers once per bundle (#5069)' accurately summarizes the main change: emitting shared FFI helpers once per widget bundle instead of per-widget.
Description check ✅ Passed The description includes all required template sections: Summary (explaining the issue and fix), Changes (three modified files detailed), Related issue (#5069), Test plan (checklist items), and Checklist compliance.
Linked Issues check ✅ Passed All coding requirements from #5069 are met: shared FFI helpers emitted once per bundle as internal code, per-widget glue slimmed to provider imports, PerryWidgetRuntime.swift generated once with app-group resolution, and three unit tests added.
Out of Scope Changes check ✅ Passed All changes are directly aligned with #5069 objectives: splitting emit_glue, adding emit_shared_runtime, refactoring compilation drivers, and adding tests—no unrelated modifications present.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/issue-5069-v6k5t6

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
crates/perry/src/commands/compile/targets.rs (2)

297-302: ⚡ Quick win

Consider validating consistent app group usage across widgets.

The current implementation uses the first widget's app_group for the bundle-wide @_cdecl shared-storage bridge. If different widgets specify different app groups, they will all share the first widget's suite, which may cause unexpected shared-storage behavior.

Consider adding a validation pass to ensure all widgets either:

  • Use the same app group, or
  • Don't specify an app group

This validation is out of scope for the current PR (which focuses on fixing the duplicate-symbol/inaccessibility regression), but would help catch configuration errors at compile time rather than runtime.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry/src/commands/compile/targets.rs` around lines 297 - 302, In the
condition where you check if any widget has an app_group or provider_func_name,
add a validation step after finding the app_group value via find_map to ensure
all widgets that specify an app_group use the same one. Implement a check that
iterates through all widgets and verifies either they all have no app_group,
they all have the same app_group value, or raises a compilation error if
inconsistencies are detected. This prevents the scenario where different widgets
with different app_group values would silently use the first widget's app_group
for the shared-storage bridge, which could cause unexpected runtime behavior.

564-569: ⚡ Quick win

Consider validating consistent app group usage across widgets.

Same consideration as the iOS widget path (lines 297-302): multiple widgets with different app groups will all use the first widget's suite in the shared-storage bridge. Consider validation to catch configuration errors, though this is out of scope for the current PR.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry/src/commands/compile/targets.rs` around lines 564 - 569, Add
validation to ensure consistent app_group usage across all widgets in the
SwiftUI target path. Before calling emit_shared_runtime with the app_group
derived from find_map, validate that all widgets which have an app_group
specified use the same value. If inconsistencies are detected, either error out
or log a warning to prevent silent misconfiguration where multiple widgets with
different app groups would incorrectly use only the first widget's app group in
the shared-storage bridge. This validation should mirror the consistency
checking that should also be applied to the iOS widget path (lines 297-302).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@crates/perry/src/commands/compile/targets.rs`:
- Around line 297-302: In the condition where you check if any widget has an
app_group or provider_func_name, add a validation step after finding the
app_group value via find_map to ensure all widgets that specify an app_group use
the same one. Implement a check that iterates through all widgets and verifies
either they all have no app_group, they all have the same app_group value, or
raises a compilation error if inconsistencies are detected. This prevents the
scenario where different widgets with different app_group values would silently
use the first widget's app_group for the shared-storage bridge, which could
cause unexpected runtime behavior.
- Around line 564-569: Add validation to ensure consistent app_group usage
across all widgets in the SwiftUI target path. Before calling
emit_shared_runtime with the app_group derived from find_map, validate that all
widgets which have an app_group specified use the same value. If inconsistencies
are detected, either error out or log a warning to prevent silent
misconfiguration where multiple widgets with different app groups would
incorrectly use only the first widget's app group in the shared-storage bridge.
This validation should mirror the consistency checking that should also be
applied to the iOS widget path (lines 297-302).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 706f4d64-98e1-408c-8bbf-b72db9f953ac

📥 Commits

Reviewing files that changed from the base of the PR and between c970fc5 and 63438bd.

📒 Files selected for processing (3)
  • crates/perry-codegen-swiftui/src/emit.rs
  • crates/perry-codegen-swiftui/src/lib.rs
  • crates/perry/src/commands/compile/targets.rs

proggeramlug pushed a commit that referenced this pull request Jun 14, 2026
…g app groups

Address CodeRabbit review on #5119: the per-bundle PerryWidgetRuntime.swift
emission was duplicated verbatim in the ios-widget and watchos-widget drivers.
Extract it into write_shared_widget_runtime(), and — since the @_cdecl
shared-storage bridge can bind only one app group per bundle — warn (non-fatal)
when widgets declare differing app groups instead of silently routing them all
through the first widget's suite.

https://claude.ai/code/session_01JJfeShA3WCh9HMiEBmqq39

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/perry/src/commands/compile/targets.rs`:
- Around line 418-436: The conflicting app groups warning is guarded by an `if
matches!(format, OutputFormat::Text)` condition, which prevents the warning from
being emitted when using JSON output format. This silently hides important
information about shared storage routing discrepancies. Remove the
OutputFormat::Text guard so the warning logic is executed regardless of the
output format chosen by the user. The eprintln! call already ensures stdout
remains clean for JSON serialization, so the guard is unnecessary and only
suppresses critical warnings in JSON mode.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: db1bd4bd-3dc4-4b13-b831-20ce17484f6b

📥 Commits

Reviewing files that changed from the base of the PR and between 63438bd and 950e173.

📒 Files selected for processing (1)
  • crates/perry/src/commands/compile/targets.rs

Comment thread crates/perry/src/commands/compile/targets.rs Outdated
proggeramlug pushed a commit that referenced this pull request Jun 14, 2026
Address CodeRabbit review on #5119: the warning used eprintln! (stderr), so the
OutputFormat::Text guard only suppressed a genuine misconfiguration signal under
--format json without protecting the JSON on stdout. Drop the guard (and the now
-unused format parameter) so the warning surfaces regardless of output format.

https://claude.ai/code/session_01JJfeShA3WCh9HMiEBmqq39
claude added 2 commits June 14, 2026 11:06
A perry/widget bundle with ≥2 Widget({...}) declarations failed under
`swiftc *.swift`: the shared perry-runtime FFI helpers and the
@_cdecl("perry_widget_shared_storage_get") bridge were emitted once per
widget into each {Name}Glue.swift. Since a bundle compiles as one Swift
module, `private` made them inaccessible from the sibling {Name}.swift
call sites, and non-private produced N redeclarations across N glue files.

Emit the shared FFI block + @_cdecl bridge once per bundle, internal, into
PerryWidgetRuntime.swift (emit_shared_runtime). Per-widget glue keeps only
the widget-unique @_silgen_name provider import. The ios/watchos widget
drivers write the shared file once after the per-widget loop, gated on any
widget using a native provider or shared storage, with the bundle-wide app
group resolved from the first widget that configures one.

https://claude.ai/code/session_01JJfeShA3WCh9HMiEBmqq39
…g app groups

Address CodeRabbit review on #5119: the per-bundle PerryWidgetRuntime.swift
emission was duplicated verbatim in the ios-widget and watchos-widget drivers.
Extract it into write_shared_widget_runtime(), and — since the @_cdecl
shared-storage bridge can bind only one app group per bundle — warn (non-fatal)
when widgets declare differing app groups instead of silently routing them all
through the first widget's suite.

https://claude.ai/code/session_01JJfeShA3WCh9HMiEBmqq39
proggeramlug pushed a commit that referenced this pull request Jun 14, 2026
Address CodeRabbit review on #5119: the warning used eprintln! (stderr), so the
OutputFormat::Text guard only suppressed a genuine misconfiguration signal under
--format json without protecting the JSON on stdout. Drop the guard (and the now
-unused format parameter) so the warning surfaces regardless of output format.

https://claude.ai/code/session_01JJfeShA3WCh9HMiEBmqq39
@proggeramlug proggeramlug force-pushed the claude/issue-5069-v6k5t6 branch from 1043adf to 431f60b Compare June 14, 2026 11:06
Address CodeRabbit review on #5119: the warning used eprintln! (stderr), so the
OutputFormat::Text guard only suppressed a genuine misconfiguration signal under
--format json without protecting the JSON on stdout. Drop the guard (and the now
-unused format parameter) so the warning surfaces regardless of output format.

https://claude.ai/code/session_01JJfeShA3WCh9HMiEBmqq39
@proggeramlug proggeramlug force-pushed the claude/issue-5069-v6k5t6 branch from 431f60b to 6ee593e Compare June 14, 2026 12:20
@proggeramlug proggeramlug merged commit 35b2e58 into main Jun 14, 2026
15 checks passed
@proggeramlug proggeramlug deleted the claude/issue-5069-v6k5t6 branch June 14, 2026 13:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants