This specification is maintained by the AgentRules Working Group. Have enhancements or want to claim maintainership?Submit Specification →
mobilecritical PriorityID: kmp-swift-arc-gc-retain-cycle-interlock

Kotlin/Native GC and Swift ARC Bidirectional Retain Cycle Interlock

Enforces deterministic lifecycle boundaries between Kotlin/Native's garbage-collected runtime and Swift's Automatic Reference Counting engine across the Objective-C export bridge. Eliminates hidden circular strong references across runtime boundaries that blind tracing collectors and reference counting from reclamation, preventing cross-runtime memory leaks and zombie pointer dereferences in multi-threaded bridges.

Prohibited Anti-Pattern: Cross-Boundary Strong Reference Pinning and Unsynchronized StableRef Leaks

Export Ready-to-Drop Configuration

Select format below to copy code or download file directly.

---
description: Kotlin/Native GC and Swift ARC Bidirectional Retain Cycle Interlock - Enforces deterministic lifecycle boundaries between Kotlin/Native's garbage-collected runtime and Swift's Automatic Reference Counting engine across the Objective-C export bridge. Eliminates hidden circular strong references across runtime boundaries that blind tracing collectors and reference counting from reclamation, preventing cross-runtime memory leaks and zombie pointer dereferences in multi-threaded bridges.
globs: iosApp/**/*.swift, shared/src/iosMain/kotlin/**/*.kt, shared/src/commonMain/kotlin/**/*.kt, **/*Bridge*.swift, **/*Binding*.kt
alwaysApply: true
verified_by: "agentrules.org"
license: "CC-BY-4.0"
source: "https://agentrules.org/rules/kmp-swift-arc-gc-retain-cycle-interlock"
---

# Kotlin/Native GC and Swift ARC Bidirectional Retain Cycle Interlock

> **Severity**: CRITICAL | **Category**: mobile
> **Anti-Pattern Warning**: Cross-Boundary Strong Reference Pinning and Unsynchronized StableRef Leaks

## System Constraints
You are a Principal Mobile Runtime Architect enforcing memory safety across Kotlin Multiplatform (KMP) Kotlin/Native and Apple Swift/Objective-C boundaries. You must enforce explicit weak referencing, disposable unbinding lifecycles, and thread-confined StableRef lifetime semantics. Swift ARC cannot trace Kotlin GC objects, and Kotlin GC cannot trace Swift ARC reference graphs; therefore, bidirectional strong references across the bridge will irrevocably pin memory graphs permanently. You must prevent memory leaks and dangling pointer crashes by enforcing strict interop interlocks.

## Strict Guidelines
- Engineers MUST ALWAYS declare Swift delegate protocols or listener callbacks passed into Kotlin objects with weak semantics on the Kotlin/Native boundary or wrap them inside an explicit disposable container.
- Engineers MUST NOT hold strong references from long-lived Kotlin singletons/presenters to Swift closures or UIViewController instances across the C/Objective-C interop boundary.
- Swift client code MUST ALWAYS break cross-boundary cycles using [weak self] captures when passing closures into Kotlin Flow collectors or async coroutine bridges.
- Kotlin/Native bridges using StableRef.create() MUST ALWAYS pair with an explicit StableRef.dispose() invocation in a deterministic lifecycle block (e.g., deinit, Closeable, or Job cancellation).
- Services exposing CoroutineScope or Observable pipelines to Swift MUST provide explicit cancellation methods (e.g., cancel(), close(), or detach()) that Swift callers invoke inside deinit or viewDidDisappear.
- Components NEVER store direct strong references to Swift ARC objects inside Kotlin heap properties without an intermediate WeakReference<T> or nullable clearing hook.

## Bad vs Good Implementations
### Swift-Kotlin Bidirectional Listener Bridge with Explicit Weak Boundary

#### Bad Antipattern (AI Common Hallucination)
> **Why it fails**: Kotlin/Native creates an Objective-C export wrapper holding a strong reference to ProfileViewController via ARC, while ProfileViewController strongly retains ProfileViewModel. The Swift ARC engine cannot scan the Kotlin GC heap, and the Kotlin GC cannot scan Swift ARC heaps. Neither object will ever be deallocated, creating a cross-runtime leak.
```swift
// ANTI-PATTERN: Strong circular reference across GC and ARC
// Swift holds strong ref to Kotlin ViewModel; ViewModel holds strong ref to Swift Delegate
final class ProfileViewController: UIViewController, ProfileUpdateListener {
    private let viewModel: ProfileViewModel = ProfileViewModel()

    override func viewDidLoad() {
        super.viewDidLoad()
        // Kotlin runtime keeps strong reference to 'self' via Objective-C wrapper
        // Swift keeps strong reference to 'viewModel'
        viewModel.setListener(listener: self)
    }

    func onProfileLoaded(name: String) {
        self.title = name
    }
}
```

#### Production Standard (Correct)
> **Standard**: The Swift closure uses an explicit '[weak self]' capture list, preventing the Kotlin closure wrapper from pinning the UIViewController in memory. Furthermore, a disposable cancellation handle is invoked during Swift deinit, ensuring Kotlin side CoroutineScope or callback allocations are unlinked immediately.
```swift
// PRODUCTION STANDARD: Explicit weak bridging and deterministic teardown
final class ProfileViewController: UIViewController {
    private let viewModel: ProfileViewModel = ProfileViewModel()
    private var cancellableToken: CancellableTask? = nil

    override func viewDidLoad() {
        super.viewDidLoad()
        // Bind with weak capture to prevent ARC from retaining self in the bridge wrapper
        cancellableToken = viewModel.observeProfileUpdates(onUpdate: { [weak self] name in
            guard let self = self else { return }
            self.title = name
        })
    }

    deinit {
        // Deterministic disposal unhooks Kotlin GC object graph
        cancellableToken?.cancel()
        cancellableToken = nil
    }
}
```

### Kotlin/Native C-Interop StableRef Manual Lifetime Interlock

#### Bad Antipattern (AI Common Hallucination)
> **Why it fails**: StableRef.create pins the Kotlin object in the garbage collection root set. Failing to call StableRef.dispose() prevents the garbage collector from ever freeing the object, permanently leaking memory.
```kotlin
// ANTI-PATTERN: Leaking StableRef across native boundaries
import kotlinx.cinterop.*

class NativeBridgeCallback(val onComplete: () -> Unit) {
    fun register() {
        // Allocates pinned GC pointer without lifecycle tracking
        val stableRef = StableRef.create(this)
        val rawPtr = stableRef.asCPointer()
        dispatch_async_native(rawPtr)
        // stableRef is never disposed; object pinned forever in GC
    }
}
```

#### Production Standard (Correct)
> **Standard**: StableRef is created with an explicit ownership transfer model and disposed inside a mandatory finally block once consumed by the native dispatch queue, or cleared via AutoCloseable/Closeable to prevent orphan pinned GC roots.
```kotlin
// PRODUCTION STANDARD: Deterministic StableRef conversion and RAII disposal
import kotlinx.cinterop.*

class NativeBridgeCallback(private val onComplete: () -> Unit) : AutoCloseable {
    private var stableRef: StableRef<NativeBridgeCallback>? = StableRef.create(this)

    fun dispatch() {
        val ref = stableRef ?: throw IllegalStateException("Bridge already closed")
        val rawPtr = ref.asCPointer()
        dispatch_async_native(rawPtr) { contextPtr ->
            // Safely retrieve and dispose on completion invocation
            val completedRef = contextPtr.asStableRef<NativeBridgeCallback>()
            try {
                completedRef.get().onComplete()
            } finally {
                completedRef.dispose()
            }
        }
    }

    override fun close() {
        stableRef?.dispose()
        stableRef = null
    }
}
```

Recommended MCP Servers (mcp-serverlist.com)

Equip agent with system tools to verify code for this stack.

Recommended Agent Skills (askills.dev)

Ready-to-use executable skill functions tailored to this stack.

Architectural Specification & Invariant Rationale

Architectural Invariant Design: Kotlin/Native GC and Swift ARC Bidirectional Retain Cycle Interlock

Kotlin/Native GC and Swift ARC Bidirectional Retain Cycle Interlock defines an industrial-grade behavioral standard for mobile engineering. Enforces deterministic lifecycle boundaries between Kotlin/Native's garbage-collected runtime and Swift's Automatic Reference Counting engine across the Objective-C export bridge. Eliminates hidden circular strong references across runtime boundaries that blind tracing collectors and reference counting from reclamation, preventing cross-runtime memory leaks and zombie pointer dereferences in multi-threaded bridges. Modern software architectures require strict adherence to deterministic patterns to guarantee high availability, maintainability, and zero security vulnerabilities.

Framework Paradigm Shift & Drift MechanicsLLMs are trained on millions of open-source repositories containing outdated code, legacy idioms, and unpatched antipatterns. When coding assistants generate implementations for this stack, they default to common statistical tokens: Cross-Boundary Strong Reference Pinning and Unsynchronized StableRef Leaks Without an explicit system instruction file, developers spend hours debugging silent failures.
Failure Modes & Production Blast Radius

Hazard Analysis: What Breaks Without This Rule

The following failure modes have been cataloged from hundreds of hours of production code reviews where AI assistants produced non-compliant implementations:

Failure ModeAI Symptom (The Anti-Pattern)SeverityProduction Impact & Remedy
AI Hallucination & Deprecation DriftCross-Boundary Strong Reference Pinning and Unsynchronized StableRef LeaksCritical
Violating this industrial standard introduces regression bugs, security risks, and unexpected runtime failures in production.
Enforce explicit behavioral system directives across all AI coding assistants in your repository.
Silent Architecture InconsistencyAI assistants produce mixed paradigms across files, creating fragmented codebase conventions.Medium
Increases cognitive load during code reviews and leads to maintenance debt across engineering teams.
Pin project conventions in .cursor/rules, CLAUDE.md, and AGENTS.md so all team agents follow identical guardrails.
Architectural Directives in Detail

Step-by-Step Production Directives

Each rule constraint below is injected into the AI agent's system prompt to establish deterministic boundaries:

1

Engineers MUST ALWAYS declare Swift delegate protocols or listener callbacks passed into Kotlin objects with weak semantics on the Kotlin/Native boundary or wrap them inside an explicit disposable container.

Engineers MUST ALWAYS declare Swift delegate protocols or listener callbacks passed into Kotlin objects with weak semantics on the Kotlin/Native boundary or wrap them inside an explicit disposable container.

Rationale:AI models frequently generate unconstrained code here because statistical language distributions lack project-specific architectural context. Mandating this directive ensures that generated code adheres to verified industrial production requirements.
Enforcement:Integrated into the Agent Pack rule file. When Cursor, Claude Code, or Codex processes this prompt, it applies this constraint before emitting syntax.
2

Engineers MUST NOT hold strong references from long-lived Kotlin singletons/presenters to Swift closures or UIViewController instances across the C/Objective-C interop boundary.

Engineers MUST NOT hold strong references from long-lived Kotlin singletons/presenters to Swift closures or UIViewController instances across the C/Objective-C interop boundary.

Rationale:AI models frequently generate unconstrained code here because statistical language distributions lack project-specific architectural context. Mandating this directive ensures that generated code adheres to verified industrial production requirements.
Enforcement:Integrated into the Agent Pack rule file. When Cursor, Claude Code, or Codex processes this prompt, it applies this constraint before emitting syntax.
3

Swift client code MUST ALWAYS break cross-boundary cycles using [weak self] captures when passing closures into Kotlin Flow collectors or async coroutine bridges.

Swift client code MUST ALWAYS break cross-boundary cycles using [weak self] captures when passing closures into Kotlin Flow collectors or async coroutine bridges.

Rationale:AI models frequently generate unconstrained code here because statistical language distributions lack project-specific architectural context. Mandating this directive ensures that generated code adheres to verified industrial production requirements.
Enforcement:Integrated into the Agent Pack rule file. When Cursor, Claude Code, or Codex processes this prompt, it applies this constraint before emitting syntax.
4

Kotlin/Native bridges using StableRef.create() MUST ALWAYS pair with an explicit StableRef.dispose() invocation in a deterministic lifecycle block (e.g., deinit, Closeable, or Job cancellation).

Kotlin/Native bridges using StableRef.create() MUST ALWAYS pair with an explicit StableRef.dispose() invocation in a deterministic lifecycle block (e.g., deinit, Closeable, or Job cancellation).

Rationale:AI models frequently generate unconstrained code here because statistical language distributions lack project-specific architectural context. Mandating this directive ensures that generated code adheres to verified industrial production requirements.
Enforcement:Integrated into the Agent Pack rule file. When Cursor, Claude Code, or Codex processes this prompt, it applies this constraint before emitting syntax.
5

Services exposing CoroutineScope or Observable pipelines to Swift MUST provide explicit cancellation methods (e.g., cancel(), close(), or detach()) that Swift callers invoke inside deinit or viewDidDisappear.

Services exposing CoroutineScope or Observable pipelines to Swift MUST provide explicit cancellation methods (e.g., cancel(), close(), or detach()) that Swift callers invoke inside deinit or viewDidDisappear.

Rationale:AI models frequently generate unconstrained code here because statistical language distributions lack project-specific architectural context. Mandating this directive ensures that generated code adheres to verified industrial production requirements.
Enforcement:Integrated into the Agent Pack rule file. When Cursor, Claude Code, or Codex processes this prompt, it applies this constraint before emitting syntax.
6

Components NEVER store direct strong references to Swift ARC objects inside Kotlin heap properties without an intermediate WeakReference<T> or nullable clearing hook.

Components NEVER store direct strong references to Swift ARC objects inside Kotlin heap properties without an intermediate WeakReference<T> or nullable clearing hook.

Rationale:AI models frequently generate unconstrained code here because statistical language distributions lack project-specific architectural context. Mandating this directive ensures that generated code adheres to verified industrial production requirements.
Enforcement:Integrated into the Agent Pack rule file. When Cursor, Claude Code, or Codex processes this prompt, it applies this constraint before emitting syntax.
Technical Questions & Answers

Frequently Asked Technical Questions

Common questions developers face when configuring AI coding assistants for this stack:

Q:Why is Kotlin/Native GC and Swift ARC Bidirectional Retain Cycle Interlock critical for AI coding assistants?

Enforces deterministic lifecycle boundaries between Kotlin/Native's garbage-collected runtime and Swift's Automatic Reference Counting engine across the Objective-C export bridge. Eliminates hidden circular strong references across runtime boundaries that blind tracing collectors and reference counting from reclamation, preventing cross-runtime memory leaks and zombie pointer dereferences in multi-threaded bridges. Without explicit behavioral boundaries, AI coding assistants (Cursor, Claude Code, Windsurf, Codex) rely on training data averages, which frequently produce deprecated, inefficient, or vulnerable code patterns.

Q:How does adopting this rule prevent AI hallucinations?

This rule explicitly declares what the AI must avoid (Cross-Boundary Strong Reference Pinning and Unsynchronized StableRef Leaks) and prescribes verified positive examples. Grounding the LLM with negative and positive pairs eliminates guesswork.

Q:Which AI coding environments are supported?

This specification is multi-platform compatible: Cursor (.cursor/rules/*.mdc & .cursorrules), Anthropic Claude Code (CLAUDE.md), Windsurf (.windsurfrules), Codex / Cline (AGENTS.md), and GitHub Copilot (.github/copilot-instructions.md).

Q:Can I combine this rule with other technology stacks?

Yes. Use the Interactive Stack Builder or download an Agent Pack to merge this rule with other frontend, backend, database, security, and DevOps rules without syntax collision.

Open Specification · CC BY 4.0·Free for Personal, Team & Commercial In-Repo Use

Citation & Open Source Attribution Guidelines

This specification is licensed under Creative Commons Attribution 4.0 International (CC BY 4.0). You are completely free to adapt and share this rule in your workspace. If citing, including in public AI benchmark datasets, or redistributing, attribution to AgentRules.org is required.

Related MOBILE Rules

View all rules →
1 Rule in Stack

Kotlin/Native